Core APIs, Part 2 295
keyboardDidShow = () => {
this.setState({
keyboardWillShow: false,
keyboardVisible: true,
});
// ...
};
keyboardWillHide = (event) => {
this.setState({ keyboardWillHide: true });
// ...
};
keyboardDidHide = () => {
this.setState({
keyboardWillHide: false,
keyboardVisible: false
});
};
// ...
The listeners keyboardWillShow, keyboardDidShow, and keyboardWillHide will each be called with
an event object, which we can use to measure the contentHeight and keyboardHeight. Let’s
do that now, using this.measure(event) as a placeholder for the function which will perform
measurements.
messaging/components/KeyboardState.js
// ...
keyboardWillShow = (event) => {
this.setState({ keyboardWillShow: true });
this.measure(event);
};
keyboardDidShow = (event) => {
this.setState({
keyboardWillShow: false,
keyboardVisible: true,
});
this.measure(event);
Core APIs, Part 2 296
};
keyboardWillHide = (event) => {
this.setState({ keyboardWillHide: true });
this.measure(event);
};
// ...
For iOS it would be sufficient to calculate measurements in the keyboardWill* events, since the
keyboardDid* events should receive the same event parameter. However, since Android only
supports the keyboardDid* events, we also need to use keyboardDidShow. Calculating measurements
in keyboardDidShow on iOS shouldn’t affect the app’s behavior, but we could do this conditionally
by checking Platform.OS === 'android' if we preferred.
We can use these events to keep track of the keyboard’s current state. Each event object will have
the following properties:
duration - Duration of the keyboard animation. In practice, this is typically constant across all
keyboard animations. This property only exists on iOS, so we’ll use a constant to approximate
it on Android.
easing - Easing curve used by the keyboard animation. This will be the special easing curve
called 'keyboard', which we can use to sync our own animations with the keyboard’s. This
property only exists on iOS, since there isn’t a specific keyboard animation on Android. We’ll
use 'easeInEaseOut' as a pleasant-looking default to approximate the keyboard animation on
Android.
startCoordinates, endCoordinates - An object containing keys height, width, screenX, and
screenY. These refer to the start and end coordinates of the keyboard. Normally height, width,
and screenX will stay the same. We can use height to determine the height of the keyboard. The
screenY value refers to the top of the keyboard, which we can use to determine the remaining
height available to render content.
To calculate the contentHeight, we can take the screenY (top coordinate of the keyboard) and
subtract layout.y (top coordinate of our messaging component).
Core APIs, Part 2 297
1 measure = (event) => {
2 const { layout } = this.props;
3
4 const { endCoordinates: { height, screenY }, duration = INITIAL_ANIMATION_DURATION\
5 } = event;
6
7 this.setState({
8 contentHeight: screenY - layout.y,
9 keyboardHeight: height,
10 keyboardAnimationDuration: duration,
11 });
12 };
13
14 // ...
Remember, y coordinates lower down on the screen are larger than those higher on the
screen, so this calculation will resulting in a positive value.
Note that if a hardware keyboard is connected, the height of the keyboard will be 0 we’ll have to
handle this specially later.
Let’s propagate all of these values into the children of this component. We’ll also propagate the
height of the entire component as containerHeight.
messaging/components/KeyboardState.js
// ...
render() {
const { children, layout } = this.props;
const {
contentHeight,
keyboardHeight,
keyboardVisible,
keyboardWillShow,
keyboardWillHide,
keyboardAnimationDuration,
} = this.state;
return children({
containerHeight: layout.height,
contentHeight,
keyboardHeight,
keyboardVisible,
Core APIs, Part 2 298
keyboardWillShow,
keyboardWillHide,
keyboardAnimationDuration,
});
}
// ...
When we use this component, it’ll look roughly like this: we’ll first wrap it in our MeasureLayout
component, and pass the layout in as a prop. We can then render our content using the keyboardInfo
object.
<MeasureLayout>
{layout => (
<KeyboardState layout={layout}>
{keyboardInfo => /* ... */}
</KeyboardState>
)}
</MeasureLayout>
Alright, we’re almost there! We have MeasureLayout and KeyboardState. The last component we
need is MessagingContainer to render the content using the sizes we’ve calculated.
MessagingContainer
Let’s create a new component MessagingContainer to render the correct Input Method Editor (IME)
at the correct size.
Once again, let’s figure out the propTypes first. This component is going to have a lot of props, since
it’s consuming data from the previous components we wrote, in addition to more props which we’ll
pass in from App.
The main job of this component is to display the correct IME at any given time. Let’s define constants
for each potential state:
NONE - Don’t show any IME.
KEYBOARD - The text input is focused, so the keyboard should be visible.
CUSTOM - Show our custom IME. In this case, we’ll show our image picker, but we could show
other kinds of input here if we wanted to.
Let’s create an object to hold these. We’ll also export it so that other components can easily use the
correct string values.
Core APIs, Part 2 299
messaging/components/MessagingContainer.js
import { BackHandler, LayoutAnimation, Platform, UIManager, View } from 'react-nativ\
e';
import PropTypes from 'prop-types';
import React from 'react';
export const INPUT_METHOD = {
NONE: 'NONE',
KEYBOARD: 'KEYBOARD',
CUSTOM: 'CUSTOM',
};
// ...
Now for the propTypes. We’ll begin by declaring each of the values that will be passed from
KeyboardState. We’ll define inputMethod and onChangeInputMethod to handle switching between
IMEs and notifying the parent of changes. We’ll also support rendering content in both the keyboard
area with renderInputMethodEditor and the main content area with children. In this case, children
should be a normal React element rather than a function like in the two components we wrote
previously.
messaging/components/MessagingContainer.js
// ...
export default class MessagingContainer extends React.Component {
static propTypes = {
// From `KeyboardState`
containerHeight: PropTypes.number.isRequired,
contentHeight: PropTypes.number.isRequired,
keyboardHeight: PropTypes.number.isRequired,
keyboardVisible: PropTypes.bool.isRequired,
keyboardWillShow: PropTypes.bool.isRequired,
keyboardWillHide: PropTypes.bool.isRequired,
keyboardAnimationDuration: PropTypes.number.isRequired,
// Managing the IME type
inputMethod: PropTypes.oneOf(Object.values(INPUT_METHOD)).isRequired,
onChangeInputMethod: PropTypes.func,
// Rendering content
children: PropTypes.node,
renderInputMethodEditor: PropTypes.func.isRequired,
Core APIs, Part 2 300
};
static defaultProps = {
children: null,
onChangeInputMethod: () => {},
};
// ...
}
Now let’s use componentWillReceiveProps to handle switching the inputMethod. When the key-
board transitions from hidden to visible, we want to set the inputMethod to INPUT_METHOD.KEYBOARD.
When the keyboard transitions from visible to hidden, we want to set the inputMethod to INPUT_-
METHOD.NONE unless we’re currently displaying the image picker (the keyboard should always be
hidden when we display the image picker, so we can ignore this transition).
messaging/components/MessagingContainer.js
// ...
componentWillReceiveProps(nextProps) {
const { onChangeInputMethod } = this.props;
if (!this.props.keyboardVisible && nextProps.keyboardVisible) {
// Keyboard shown
onChangeInputMethod(INPUT_METHOD.KEYBOARD);
} else if (
// Keyboard hidden
this.props.keyboardVisible &&
!nextProps.keyboardVisible &&
this.props.inputMethod !== INPUT_METHOD.CUSTOM
) {
onChangeInputMethod(INPUT_METHOD.NONE);
}
// ... more to come!
}
// ...
Since inputMethod will be stored in the state of the parent, we’ll call onChangeInputMethod
and let the parent pass this prop back down. We could store inputMethod in the state of
Core APIs, Part 2 301
MessagingContainer, but since the parent needs to access this value, it’s best that the parent stores
it.
LayoutAnimation
We’re going to use LayoutAnimation to handle automatically transitioning between the various
states of this component. LayoutAnimation is still considered experimental, and it’s more common
to use the other animated API, Animated. However, LayoutAnimation is the only way we can match
the exact animation of the keyboard. It’s used internally by the built-in KeyboardAvoidingView
component, so it’s safe for us to use despite being considered experimental.
Currently LayoutAnimation is disabled by default on Android, so we need to enable it by calling
UIManager.setLayoutAnimationEnabledExperimental(true). We can enable it anywhere in the app,
but let’s do it at the top of MessagingContainer.js, since that’s the file we use it in:
messaging/components/MessagingContainer.js
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
The UIManager object contains a variety of APIs for getting access to native UI elements
for measuring, but we won’t use it for anything else here.
LayoutAnimation automatically handles animating elements that should change size or appear/dis-
appear between calls to render. We call LayoutAnimation.create to define an animation configu-
ration, and then LayoutAnimation.configureNext to enqueue the animation to run the next time
render is called.
The LayoutAnimation.create API takes three parameters:
duration - The duration of the animation
easing - The curve of the animation. We choose from a predefined set of curves: spring, linear,
easeInEaseOut, easeIn, easeOut, keyboard. The keyboard curve is the key to matching the
keyboard’s animation curve although it only exists on iOS.
creationProp - The style to animate when a new element is added: opacity or scaleXY.
In our case, we want to call this every time the component will re-render, so componentWillReceiveProps
is the best place.
Core APIs, Part 2 302
messaging/components/MessagingContainer.js
// ...
componentWillReceiveProps(nextProps) {
// ... from before!
const { keyboardAnimationDuration } = nextProps;
const animation = LayoutAnimation.create(
keyboardAnimationDuration,
Platform.OS === 'android' ? LayoutAnimation.Types.easeInEaseOut : LayoutAnimatio\
n.Types.keyboard,
LayoutAnimation.Properties.opacity,
);
LayoutAnimation.configureNext(animation);
}
// ...
LayoutAnimation applies to the entire component hierarchy, not just the component we call it from,
so this will actually animate every component in our app. It may be a better idea to selectively choose
when to animate based on the exact props which have changed, but for simplicity, let’s assume we
always want to animate.
Handling the back button
We should add one last bit of logic in MessagingContainer.js to handle the hardware back button
on Android. When the CUSTOM IME is active, we want the back button to dismiss the IME, just like it
would for the device keyboard. We’ll use BackHandler for this. When the back button is pressed, if
the CUSTOM IME is active, we’ll call onChangeInputMethod(INPUT_METHOD.NONE) to notify the parent.
messaging/components/MessagingContainer.js
// ...
componentDidMount() {
this.subscription = BackHandler.addEventListener('hardwareBackPress', () => {
const { onChangeInputMethod, inputMethod } = this.props;
if (inputMethod === INPUT_METHOD.CUSTOM) {
onChangeInputMethod(INPUT_METHOD.NONE);
return true;
Core APIs, Part 2 303
}
return false;
});
}
componentWillUnmount() {
this.subscription.remove();
}
// ...
Rendering the MessagingContainer
Now let’s render this thing! We’ll render an outer View which contains the message list and the tool-
bar (via children), and an inner View which renders the image picker (via renderInputMethodEditor).
The conditional logic is pretty complex, so let’s take a look at it in-line with the code.
messaging/components/MessagingContainer.js
// ...
render() {
const {
children,
renderInputMethodEditor,
inputMethod,
containerHeight,
contentHeight,
keyboardHeight,
keyboardWillShow,
keyboardWillHide,
} = this.props;
// For our outer `View`, we want to choose between rendering at full
// height (`containerHeight`) or only the height above the keyboard
// (`contentHeight`). If the keyboard is currently appearing
// (`keyboardWillShow` is `true`) or if it's fully visible
// (`inputMethod === INPUT_METHOD.KEYBOARD`), we should use
// `contentHeight`.
const useContentHeight =
keyboardWillShow || inputMethod === INPUT_METHOD.KEYBOARD;
Core APIs, Part 2 304
const containerStyle = {
height: useContentHeight ? contentHeight : containerHeight,
};
// We want to render our custom input when the user has pressed the camera
// button (`inputMethod === INPUT_METHOD.CUSTOM`), so long as the keyboard
// isn't currently appearing (which would mean the input field has received
// focus, but we haven't updated the `inputMethod` yet).
const showCustomInput =
inputMethod === INPUT_METHOD.CUSTOM && !keyboardWillShow;
// If `keyboardHeight` is `0`, this means a hardware keyboard is connected
// to the device. We still want to show our custom image picker when a
// hardware keyboard is connected, so let's set `keyboardHeight` to `250`
// in this case.
const inputStyle = {
height: showCustomInput ? keyboardHeight || 250 : 0,
};
return (
<View style={containerStyle}>
{children}
<View style={inputStyle}>{renderInputMethodEditor()}</View>
</View>
);
}
// ...
In order for the toolbar to sit above the home indicator on the iPhone X, we’ll need to adjust the
space below the toolbar as the keyboard transitions up and down.
Supporting the iPhone X
You may skip this section if you’re not testing with an iPhone X.
In the “Core Components” chapter, we used the SafeAreaView to support the iPhone X. This won’t
work here, since we want to animate the space below the toolbar (to avoid a jerk when the space
changes).
Core APIs, Part 2 305
We’ll install the npm library react-native-iphone-x-helper
65
to help us determine if the device is
an iPhone X.
In your terminal, install the library with:
yarn add react-native-iphone-x-helper@1.0.1
React Native doesn’t currently provide a way to determine if the device is an iPhone X. This
library simply checks the device’s dimensions. Hopefully in the future something better will
be provided out-of-the-box for accessing the safe area insets directly.
After this finishes, import the isIphoneX utility function at the top of the file:
messaging/components/MessagingContainer.js
import { isIphoneX } from 'react-native-iphone-x-helper';
Now we can update the render method above to include extra space below the toolbar:
messaging/components/MessagingContainer.js
// ...
render() {
// ...
// The keyboard is hidden and not transitioning up
const keyboardIsHidden =
inputMethod === INPUT_METHOD.NONE && !keyboardWillShow;
// The keyboard is visible and transitioning down
const keyboardIsHiding =
inputMethod === INPUT_METHOD.KEYBOARD && keyboardWillHide;
const inputStyle = {
height: showCustomInput ? keyboardHeight || 250 : 0,
// Show extra space if the device is an iPhone X the keyboard is not visible
marginTop: isIphoneX() && (keyboardIsHidden || keyboardIsHiding) ? 24 : 0,
};
// ...
65
https://www.npmjs.com/package/react-native-iphone-x-helper
Core APIs, Part 2 306
}
// ...
Whew, we made it. Save MessagingContainer.js. Now we just need to render MessagingContainer
from App.
Rendering MessagingContainer in App
Head back to App.js and import the components we’ve just created:
messaging/App.js
// ...
import KeyboardState from './components/KeyboardState';
import MeasureLayout from './components/MeasureLayout';
import MessagingContainer, { INPUT_METHOD } from './components/MessagingContainer';
// ...
Let’s include the inputMethod in the state of App, and handle changes to it.
messaging/App.js
// ...
export default class App extends React.Component {
state = {
// ...
inputMethod: INPUT_METHOD.NONE,
};
// ...
handleChangeInputMethod = (inputMethod) => {
this.setState({ inputMethod });
};
handlePressToolbarCamera = () => {
this.setState({
isInputFocused: false,
inputMethod: INPUT_METHOD.CUSTOM,
});
Core APIs, Part 2 307
};
// ...
}
// ...
Lastly, let’s use MeasureLayout, KeyboardState, and MessagingContainer to render the UI compo-
nents we’ve already written. We’ll rearrange this.renderMessageList,this.renderToolbar, and
this.renderInputMethodEditor so that they render within MessagingContainer.
We can update the render method of App to look like this:
// ...
render() {
const { inputMethod } = this.state;
return (
<View style={styles.container}>
<Status />
<MeasureLayout>
{layout => (
<KeyboardState layout={layout}>
{keyboardInfo => (
<MessagingContainer
{...keyboardInfo}
inputMethod={inputMethod}
onChangeInputMethod={this.handleChangeInputMethod}
renderInputMethodEditor={this.renderInputMethodEditor}
>
{this.renderMessageList()}
{this.renderToolbar()}
</MessagingContainer>
)}
</KeyboardState>
)}
</MeasureLayout>
{this.renderFullscreenImage()}
</View>
);
}
Core APIs, Part 2 308
// ...
Using the children function prop pattern, we can pretty clearly visualize the flow of data downward
into MessagingContainer.
Note that since keyboardInfo contains many properties, it’s easiest to pass them all into MessagingContainer
at once with the object spread syntax ...keyboardInfo. If we prefer, we could also assign each
property individually, e.g. keyboardHeight={keyboardInfo.keyboardHeight}.
Save App.js and test it out! You should see the same components as before, but now they animate
smoothly to avoid the keyboard as it appears and disappears.
In the default state, our app should look like this:
Tapping the input field should pop open the keyboard, and smoothly transition the rest of the UI:
Core APIs, Part 2 309
Tapping the camera icon should transition to the image picker:
Core APIs, Part 2 310
We’re Done!
We’ve built a messaging app UI complete with text messages, images, and maps. We notify the
user of connectivity issues. We display a pixel-perfect infinite scrolling grid of photos. We smoothly
animate the UI as new messages are added and removed (try it if you haven’t! the LayoutAnimation
takes care of this automatically). We handle the keyboard gracefully on both platforms.
Navigation
In the “Core Components” chapter, we explored how different parts of the app (the image feed and
user comments) can be represented as separate screens - components that take up the entire device
screen. When building screens, handling how the user navigates between them is a primary concern.
Navigation is a major piece of any mobile application with multiple screens. With a navigation
system in place, a user can access any part of an application. It also allows us to structure and
separate how data is handled in the app.
Handling navigation in a mobile application is fundamentally different from a website. For a website,
the state of a user’s location is usually kept in the browser’s URL. Although the browser maintains
a history of pages visited in order to allow the user to move back and forth, the browser only stores
page URLs and is otherwise stateless. On mobile, the entire history stack is maintained and can be
accessed.
On mobile, we have more control and flexibility over history management. We can keep a history
stack that includes details of each route including parameters and part of the application state.
Further, mobile navigation presents its own set of challenges. One of the biggest is the reduced real
estate of the user’s device screen compared to a desktop or laptop computer. We need to make sure
there are easily visible and identifiable navigation components that will allow the user to move to
another part of the application when pressed. Including a complex navigation flow comes with the
cost of a larger number of navigation components (such as menu options). For this reason, most
mobile apps tend to have a small and focused number of screens that a user can easily navigate to
and understand.
Navigation in React Native
This section will explore the landscape of navigation in React Native in some detail. If you
would like to jump straight to building our sample application, feel free to skip this section
and return to it later.
One of the primary navigation patterns in a mobile app is a stack-based pattern. In this pattern, only
one screen can be seen by the user at any given time. Navigating involves pushing the new screen
onto the navigation stack. We’ll explore stack-based navigation in more detail later in the chapter.
For now, it is important to realize that this pattern, among others, uses different native components
for iOS and Android. For example, building a stack-based navigation flow between screens can be
done using UINavigationController
66
for iOS and connecting Activities
67
for Android.
66
https://developer.apple.com/documentation/uikit/uinavigationcontroller
67
https://developer.android.com/guide/components/activities/index.html
Navigation 312
There are two primary approaches to navigation in React. We can either include actual native
iOS/Android navigational elements or use JavaScript to create the required animations and com-
ponents that we need.
Native navigation
The first way we can add navigation is to use native iOS/Android navigational components.
In an iOS application, views are used to build the UI and display content to the user. A view controller
(or the UIViewController class) is used to control a set of views and allows us to connect our UI
with our application data. By including multiple view controllers in our app, we can build different
screens as well as transition between them.
A navigation controller (UINavigationController ) simplifies the process of navigating between
screens by allowing us to pass in a stack of UIViewControllers. It will take care of including a
header navigation bar at the top of our device with a back button that allows us to pop the current
view controller off of the current stack. With this, it maintains the hierarchy of all the screens within
the stack.
Example of a navigation controller (from Apple Developer Documentation - UINavigationController)
In Android, activities are used to create single screens to define our UI. We can use tasks in order
to define a stack of activities known as the back stack. The startActivity method can be used to
start a new activity. When this happens, the activity is pushed onto the activity stack. In order to
return to the previous screen, the physical back button on every Android device can be pressed in
order to run the finish method on the activity. This closes the current activity, pops it off the stack
and returns the user back to the previous activity.
Navigation 313
Android Back Stack (from Android Developers Documentation - Tasks and Back Stack)
In React Native, all of our component code executes on a JavaScript thread. These components then
bridge to a separate main thread responsible for rendering native iOS and Android views.
In the first chapter, we briefly mentioned how we can eject from Expo if we need to include any
native dependencies ourselves. This includes any native iOS or Android code we wish to write
ourselves or third-party libraries that provide a React Native API which bridges to a specific native
module. We can use this to include native navigation in our application. We can create native
modules around platform-specific navigation components (such as UINavigationController and
Activity) and bridge them ourselves in a React Native app.
We’ll explore bridging native APIs in much more detail in the “Native Modules” chapter.
Pros
The primary benefit of this approach is a smoother navigation experience for the user. This is because
purely native iOS/Android navigation APIs can be used with all of our navigation happening within
the native thread. This approach works well when including React Native in an existing native iOS
or Android application. Using the same navigation components and transitions throughout the app
means that different screens in the app will feel consistent regardless of whether they’re written
natively or with React Native.
Additionally, if an operating system update modifies the style or functionality of navigation
components, you won’t have to wait for the same modifications to be made in your JavaScript-
based navigation library.
Cons
One of the issues with this navigation approach is that it usually involves more work. This is
because we need to ensure navigation works for both iOS and Android, using their respective native
navigational components.
Navigation 314
Moreover, we will also have to eject from Expo and take care of linking and bridging any
native modules ourselves. This means we cannot build an application with native iOS navigation
components if we do not own a Mac computer.
Another potential problem with this solution is that it can be significantly harder to modify or create
new navigation patterns. In order to customize how navigation is performed, we would have to dive
in to the native code and understand how the underlying navigation APIs work before being able
to change them.
Navigation with JavaScript
The second approach to adding navigation to a React Native app is to use JavaScript to create
components and navigation patterns that look and feel like their native counterparts. This is done
solely using React Native built-in components and the Animated API for animations.
We can explain how this works by using stack navigation as an example again. As we mentioned
earlier, this pattern allows us to move between screens by pushing a second screen on top of the
previous one. We usually see this happen by seeing the second screen slide in from either the right
or bottom edge of the device screen or fading in from the bottom. When we attempt to navigate
backwards, the current screen slides back out in the opposite direction or fades out from the top.
With the Animated API, we can use animated versions of some built-in components such as View as
well as create our own. We can create stack based navigation (as well as other navigation patterns)
by nesting our screen components within an Animated component. We can then have our screens
slide (or fade) in and out of our device when we need to allow the user to navigate throughout our
application. We’ll have to maintain the hierarchy of screens entirely in JavaScript ourselves.
Pros
One of the advantages of using JavaScript-based navigation is that it can be simpler to build the
components and animation mechanisms that can be used in both platforms instead of trying to
create a bridge to all of the core native iOS/Android APIs that we would need. This also gives us
more control and flexibility to customize specific navigation features instead of relying on what’s
available in the native platforms. We can debug any issues we experience with navigation that is
purely JavaScript-based without diving in to native code.
Most of the work during an animation using the React Native Animated API is on the JavaScript
thread. This means that every frame needs to go over the bridge to the native thread to update the
views during a transition. Fortunately, we have the option to use the API’s native driver option to
render natively driven animations. These animations are performed with animation calculations
happening on the native thread. By building navigation with this, navigation animations will
perform smoothly.
We’ll explore the built-in Animated API in greater detail in the next chapter.
Navigation 315
Another benefit of keeping all of our navigation elements within the JavaScript thread means that
we can take advantage of services such as CodePush
68
to allow us to dynamically update the
application’s JavaScript code (which includes our navigation) without rolling out a new build to
our users.
Cons
There are also disadvantages with this approach. Firstly, the app can never feel exactly like a native
application in terms of navigation. As much as we can try to mimic how navigation components
and animations look like in the native layer, there may always be slight discrepancies. This can be
a bigger problem if we happen to be including React Native components into an existing native iOS
or Android application. Building transitions between screens built natively and screens built with
React Native can be a challenge if we’re using only JavaScript for our navigation.
Another potential concern with JavaScript-based navigation is slower updates in relation to the
underlying platform’s operating system. Updates to the iOS or Android version may bring changes
to the native navigational views and components. We’ll need to make sure the views and components
built with JavaScript are also updated in order to better match how they are represented natively.
Third-party libraries
In React Native, we have the option of setting up navigation by creating our own native modules
or by building our own JavaScript-based implementation. There are also a number of community-
supported libraries that we can use for either of these approaches:
React Native Navigation
69
by Wix engineering and Native Navigation
70
by Airbnb are both
navigation libraries that provide access to native iOS/Android navigation components using a
React Native API.
React Navigation
71
and React Router
72
are two popular third-party JavaScript navigation
libraries.
Using one of these libraries can make things significantly easier than building a navigation pattern
entirely from scratch. Moreover, all of these community-built libraries are continuously maintained
with updates included in each new release.
Navigation alternatives
Not all mobile applications need to have a complete navigation architecture. Examples include an
app that only has a few screens or does not even need any navigation in the first place (such as a
68
https://github.com/Microsoft/react-native-code-push
69
https://wix.github.io/react-native-navigation/#/
70
http://airbnb.io/native-navigation/
71
https://facebook.github.io/react-native/docs/navigation.html#react-navigation
72
https://reacttraining.com/react-router/native/guides/philosophy
Navigation 316
single-screen game). However, most applications with more than a few screens will usually need
some form of navigation to allow the user to move between them.
There is no single correct solution for applications that require navigation. Different mobile apps will
always have different features and complexities. For example, it may be easier to use a JavaScript
implementation in a brand new and relatively simple application without complex navigation
requirements. However, we might find it easier to use a native solution if we plan on rolling React
Native components into a native application. We should always weigh the benefits and challenges
of each solution before deciding which approach to take.
Deprecated solutions
Navigation is a core tenet of any native application. Just like other built-in components (such as View
and Text), React Native also used to provide a number of different built-in navigation APIs. Here
are a few examples:
NavigatorIOS
a
provides an API to access the UINavigationController component for iOS to
build a screen navigation stack. It is not currently maintained and cannot be used for Android.
Navigator is a JavaScript navigation implementation that was included into React Native when
it first launched. Expo built ExNavigator on top of this API with the aim of providing more.
features. However, it did not provide a complete navigation solution and was deprecated soon
after.
NavigationExperimental is another JavaScript implementation and aimed to solve the prob-
lems noticed in Navigator. ExNavigation was built by the Expo team to act as a wrapper
around NavigationExperimental. It is also now deprecated.
It is important to note that all of these APIs are either not maintained or are deprecated. It is
recommended to use one of the newer community-built navigation libraries instead.
Since navigation is an important part of many mobile applications, all of these efforts were done in
order to provide a simple React Native API that can be imported and used directly in a component.
However, navigation is a lot more complex than many other built-in components. It is not easy to
provide a simple navigation API that can solve all navigation concerns in any application. For this
reason, a number of different open-source alternatives were created by the community. The efforts
from Navigator, NavigationExperimental, and the community-built ex-navigation were combined
to form the community-built React Navigation library.
a
https://facebook.github.io/react-native/docs/navigatorios.html
In this chapter
We covered the differences between native and JavaScript navigation implementations as well
as some of their advantages and disadvantages. For each approach, we also discussed how using
an open-source library that is continuously maintained can make things easier than building a
Navigation 317
navigation architecture from scratch.
Co-authored by the Facebook team and the open-source community, React Navigation
73
is the
recommended option in the React Native documentation. For this reason, we’ll use React Navigation,
a JavaScript-based implementation, in this chapter to handle navigation in our sample application.
Contact List
In this chapter, we’re going to build a contact list application that allows a user to view contact
information across several screens. We’ll begin by building our first screen, Contacts, that shows a
list of contact information fetched from a remote API.
Contacts
Then we’ll explore how we can allow the user to navigate to a separate Profile screen for each
specific contact.
73
https://facebook.github.io/react-native/docs/navigation.html#react-navigation
Navigation 318
Profile
We’ll then include another top level screen, Favorites, to show favorited contacts.
Navigation 319
Favorites
Then we’ll build a User screen and tie together our top level screens using a tab navigation
component.
Navigation 320
User
The last screen we’ll create will be an options screen that the user can navigate to through the user
screen.
Navigation 321
Options
By building out each screen and connecting them, we’ll get a better understanding of how a
navigation pattern such as tabs can be coupled with stack navigation. While doing so, we’ll also look
into creating a small state container that controls the entire state of our app and can be accessed in
any of our screens.
By the time we’re finished with this chapter, we will have covered a number of different navigation
patterns and show how they can fit together. We’ll also be touching on a number of core concepts
we’ve already covered in the previous chapters.
Previewing the app
Before we begin, to try the completed app on your device:
On Android, you can scan this QR code with the Expo app:
Navigation 322
QR Code
On iOS, you can navigate to the contact-list/ directory within the sample code folder and
either preview it on the iOS simulator or send the link of the project URL to your device as we
explained in the first chapter.
Spend a little time navigating between the different screens to get a feel for all the functionality.
Starting the project
Just as we did in the previous chapters, let’s create a new app with the following command:
create-react-native-app contact-list --scripts-version 1.14.0
Once this finishes, navigate into the contact-list directory and start the app.
Navigation 323
Like you did in previous chapters, copy over the
contact-list/utils
directory from the sample
code into your own project. The utils/ directory contains the following:
A few utility methods
Methods that return results from our external API, Random User Generator
74
A colors object with a number of different colors
Copy over the contact-list/components directory as well. These components are the low level
presentational components that don’t manage any state of their own. They are used in the app to
display UI elements in all of our screens. Here’s a brief overview of each of the components:
ContactListItem will be used for each contact’s list item in the Contacts screen.
ContactThumbnail renders a thumbnail for the contact avatar that can fire an action when
pressed. It can also show the user’s name and phone number underneath based on optional
props. This component will be used to render a list of user avatars in the Favorites screen as
well as show the user thumbnail in the Profile and User screens.
DetailListItem
shows a list item with a title, subtitle, and an optional icon. This component
will be used to show contact details in the Profile screen as well as mocked links in the Options
screen.
74
https://randomuser.me/
Navigation 324
Feel free to dive in and take a closer look at any of the files within utils/ and components/ to get a
better idea of how they work.
Container and Presentational components
Before we dive in to building our application, let’s take a little time to further understand how
we separate our screen and component logic. In all of our previous chapters, we explored building
custom components to create higher-level abstractions over built-in components (such as View, Text,
etc…). We can think of screens in the same way. Just like any other component, screens wrap over
lower level components. The difference here is that we can build our screen components to take up
the entire device screen and allow the user to navigate between them.
We briefly explored this pattern in the “Core Components” chapter where we built a Feed and
Comments screen for our Instagram clone app. While doing so, we managed all of our remote data
fetching within the Feed screen and the rest of our lower level components only received this
information via props. This further ties in to the pattern we’ve seen in each of the applications
we’ve built in this book so far: the concept of container components that take care of data fetching
and state management and presentational components that take in data and provide the markup
and styling in our application. We can closely follow this logic by separating how we build screens
and components in an application.
Although the Feed screen was responsible for data fetching in our Instagram clone app, we still had
some state managed in our root App component. For a relatively large application with a significant
number of screens, managing data in a single component like App may not be the most maintainable
way to handle state. For this reason, third-party state container libraries are commonly used. Instead
of using a specific library and trying to understand its APIs, we’ll handle data in our application by
writing a small custom state container. The same pattern will apply to any complex application with
a central state container regardless of which library is used.
Contacts
The first screen we’ll build is the main contacts screen which will also serve as the starting point
of our application. Create a screens/ directory and add a Contacts.js file. As we mentioned in
the “Core Components” chapter, a more complex application might be structured with directories
nested within screens/ for better categorizing of files. Since this application only consists of five
screens, we’ll add them all to the screens/ directory.
We’ll begin by defining our imports in the file:
Navigation 325
contact-list/1/screens/Contacts.js
import React from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
} from 'react-native';
import ContactListItem from '../components/ContactListItem';
import { fetchContacts } from '../utils/api';
We’ve imported a few necessary built-in components including FlatList and ActivityIndicator
as well as our custom ContactListItem component responsible for displaying each of our contacts
items in the list. Aside from components, we also import the fetchContacts method in order to
retrieve our list of contacts and our colors object from utils.
Now let’s begin creating our class component:
contact-list/1/screens/Contacts.js
export default class Contacts extends React.Component {
state = {
contacts: [],
loading: true,
error: false,
};
async componentDidMount() {
try {
const contacts = await fetchContacts();
this.setState({
contacts,
loading: false,
error: false,
});
} catch (e) {
this.setState({
loading: false,
error: true,
});
Navigation 326
}
}
We’ve set up local component state that includes a contacts array and loading/error attributes.
In here, we’ve set our initial loading property to true because we fire our API call as soon as our
component mounts. We then update this to false as soon as our request finishes successfully.
Let’s now build the UI that gets rendered on the screen:
contact-list/1/screens/Contacts.js
renderContact = ({ item }) => {
const { name, avatar, phone } = item;
return <ContactListItem name={name} avatar={avatar} phone={phone} />;
};
render() {
const { loading, contacts, error } = this.state;
const contactsSorted = contacts.sort((a, b) =>
a.name.localeCompare(b.name));
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading &&
!error && (
<FlatList
data={contactsSorted}
keyExtractor={keyExtractor}
renderItem={this.renderContact}
/>
)}
</View>
);
}
In the component render method, we sort our contacts alphabetically and show a loading indicator
if state.loading is true, an error message if state.error is true, or a list of our contacts using
FlatList. For each item in the list, we use a renderContact helper method that passes down the
contact name, avatar and phone as props to ContactListItem.
Navigation 327
We’ll also need to create our list’s keyExtractor method which we can write under our imports at
the top of the file:
contact-list/1/screens/Contacts.js
import { fetchContacts } from '../utils/api';
const keyExtractor = ({ phone }) => phone;
export default class Contacts extends React.Component {
The last thing we’ll need to do is set up the styles for this component. Since our existing
presentational component ContactListItem takes care of most of our styling, we’ll just set up styles
for the container View component:
contact-list/1/screens/Contacts.js
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
justifyContent: 'center',
flex: 1,
},
});
Try it out
To quickly take a look at how this screen renders, we can temporarily place this component within
App:
contact-list/App.js
import React from 'react';
import Contacts from './screens/Contacts';
export default function App() {
return <Contacts />;
}
Now we can see our Contacts screen if we run the app:
Navigation 328
Contacts
You may notice the top and bottom of our list touches the edges of our device screen. This will be
fixed once we introduce our header and tab navigation components to the app. Pressing any of the
contacts does not do anything just yet. We’ll explore how we can navigate to a specific contact’s
profile screen in a bit.
You may also see a warning about a missing onPress prop which is required in the ContactListItem
component. We’ll include it once we begin adding navigation to our application.
Profile
Let’s move on to building our second screen, Profile, which shows details about a specific contact.
Create a Profile.js file within the same screens directory. Again, we’ll begin with our imports:
Navigation 329
contact-list/1/screens/Profile.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import ContactThumbnail from '../components/ContactThumbnail';
import DetailListItem from '../components/DetailListItem';
import { fetchRandomContact } from '../utils/api';
import colors from '../utils/colors';
We’ve included the ContactThumbnail and DetailListItem presentational components that we’ll
need for this screen as well as the colors object we’ll use for some styling.
We’ve also included fetchRandomContact to obtain a random contact’s information. This is tempo-
rary in order to render this screen for the first time, but will be removed once we have navigation
in place and the contact ID is passed from the previous screen to the Profile screen.
We can build our class component as follows:
contact-list/1/screens/Profile.js
export default class Profile extends React.Component {
state = {
contact: {},
};
async componentDidMount() {
const contact = await fetchRandomContact();
this.setState({
contact,
});
}
render() {
const {
avatar, name, email, phone, cell,
} = this.state.contact;
return (
<View style={styles.container}>
<View style={styles.avatarSection}>
<ContactThumbnail avatar={avatar} name={name} phone={phone} />
Navigation 330
</View>
<View style={styles.detailsSection}>
<DetailListItem icon="mail" title="Email" subtitle={email} />
<DetailListItem icon="phone" title="Work" subtitle={phone} />
<DetailListItem icon="smartphone" title="Personal" subtitle={cell} />
</View>
</View>
);
}
}
We’ve defined a contact object as our only attribute in our component state. Our componentDidMount
method fires an API call to get a random contact. Again, this is temporary until we’ve included our
navigation library. This is because we’ll eventually pass the contact’s information from the Contacts
screen.
We have not included any loading or error attributes for this same reason. This is because once
navigation is in place, this screen will only be accessible through another screen by pressing on a
contact list item or thumbnail. This means there will be no loading or potential errors from data
fetching happening at this point.
Although this might usually be the case when building screens that are only accessible through
other screens in a stack, there are scenarios where we may need to fetch data in nested screens. A
good example is deep linking which allows a user to navigate to a certain part of the app through
another app or a web browser using a specific link. We’ll explore this topic later in this chapter.
The render method is relatively straightforward. We show the user thumbnail for the top half of the
screen using ContactThumbnail as our component (which accepts the user’s avatar, name, and phone
as props). For the bottom half of the screen, we’re displaying a few DetailListItem components to
show the user’s email, work, and cell numbers.
We can now create styling for the View container components we’re using for layout at the end of
the file:
contact-list/1/screens/Profile.js
const styles = StyleSheet.create({
container: {
flex: 1,
},
avatarSection: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.blue,
},
Navigation 331
detailsSection: {
flex: 1,
backgroundColor: 'white',
},
});
Try it out
Once again, let’s render our screen to see if everything is working well:
contact-list/App.js
import React from 'react';
import Profile from './screens/Profile';
export default function App() {
return <Profile />;
}
Running the app should show the Profile screen for a random contact:
Navigation 332
Profile
React Navigation
Now that we have our first two screens in place, let’s start adding navigation to our app! As we
mentioned earlier in the chapter, there are a number of different open-source navigation libraries
available. We’ll be using React Navigation
75
for this application. Since it’s purely a JavaScript
implementation, we don’t have to worry about linking iOS and Android dependencies. We can
install it to our app by using yarn:
yarn add react-navigation@1.5.11
Specify version 1.5.11 as above so that the version in your application matches the version we use
here.
Stack navigation
We briefly described how a stack navigator works earlier in this chapter. This pattern allows a user
to navigate from one screen to another by pushing the new screen to the top of the stack. The user
can also pop the current screen off the stack in order to return to the previous screen.
75
https://facebook.github.io/react-native/docs/navigation.html#react-navigation
Navigation 333
In both iOS and Android, a back button at the top of the navigation bar is how a user usually
navigates back to a previous screen by removing the current screen off the stack. On Android devices,
there is also a physical or soft key back button at the bottom of the device screen that also allows
you to go back on any application.
With this navigation flow, only one screen is visible at any given time. We can think of the entire
navigation stack as an ordered array of screens, with the last element being the screen that is
currently visible and the first element being the root screen (or the screen that is visible when loading
the app for the first time).
Let’s begin by connecting our first two screens as a single stack. We’ll define all of our navigation
logic in a separate routes.js file at the root of our entire app.
Create the file and add the following code:
contact-list/2/routes.js
import { StackNavigator } from 'react-navigation';
import Contacts from './screens/Contacts';
import Profile from './screens/Profile';
export default StackNavigator({
Contacts: {
screen: Contacts,
},
Profile: {
screen: Profile,
},
});
We pass in our route configuration as an argument to StackNavigator. The object maps route names
to their configuration. We have two routes defined above, Contacts and Profile.
For each route, we define a configuration object with just one property: screen. This is the
component we wish to render at that specific route. We’ll explore more configuration options in
a bit.
We can also pass a second argument to StackNavigator, our stack navigator configurations. We’ll
also explore this a little later in this section.
In React Navigation, every navigator including StackNavigator creates a higher-order component
that wraps over each of the screen components defined within its route configurations. It enhances
each of its components by creating a newer component with a navigation prop.
Navigation 334
Higher-Order Components
In short, higher-order components are functions that take in an existing component and
return a new component with added functionality. They’re useful for minimizing code
duplication by containing common logic in a single component that can be shared among
multiple components. They’re also useful for libraries like React Navigation.
Internally, StackNavigator() generates a higher-order component that provides each of
our screens with a navigation prop. This prop serves as the interface between our screen
components and the React Navigation library.
For more information on higher-order components, refer to its section in the Appendix.
The navigation prop provides us with the following:
navigate: Method to allow us to navigate between screens. With a StackNavigator, this
method pushes the new screen on top of the current stack.
state: Object that returns the name and identifier of the current route as well as its parameters.
setParams: Method to change the current screen’s parameters.
goBack: Method that allows us to navigate to a previous screen. For StackNavigator, this pops
the current screen (or number of screens) until the specified screen is reached within the stack.
The React Navigation documentation
76
contains more detail about the navigation prop.
Let’s now modify our App.js file to render our navigator instead of a single component:
contact-list/2/App.js
1 import React from 'react';
2
3 import AppNavigator from './routes';
4
5 export default function App() {
6 return <AppNavigator />;
7 }
Now that we’ve set up the first StackNavigator of our application, we’ll need to use our navigation
prop to allow the user to navigate from the Contacts screen to the Profile screen. We know that
the ContactListItem component contains an onPress prop that fires the action passed to it. Let’s
modify how we render this component within our Contacts screen:
76
https://reactnavigation.org/docs/navigation-prop.html
Navigation 335
contact-list/2/screens/Contacts.js
renderContact = ({ item }) => {
const { navigation: { navigate } } = this.props;
const { name, avatar, phone } = item;
return (
<ContactListItem
name={name}
avatar={avatar}
phone={phone}
onPress={() => navigate('Profile')}
/>
);
};
Try running the app and you’ll notice a header navigation bar at the top of the screen. In addition
to supplying the navigation prop, the StackNavigator HOC also renders a header above the screen
components it wraps.
Contacts
Navigation 336
Pressing any contact will navigate to the profile screen. The default behaviour for iOS is an animation
that slides the new screen from the right. For Android, the newer screen fades in from the bottom.
Profile
Pressing the back button also pops the profile screen of the stack and returns us to the contacts
screen.
In your terminal, you may be seeing a warning similar to the following:
Warning: isMounted(...) is deprecated in plain JavaScript React classes...
This warning shows when using our current version of React Navigation (1.5.11) with our version
of React Native (0.55.2). There is currently an open GitHub issue
a
where this should be resolved in
a future release.
a
https://github.com/react-navigation/react-navigation/issues/3956
Although our stack navigation pattern works, we have two problems:
Recall that we’re using the fetchRandomContact() method to obtain a random contact. This
means that pressing a specific contact doesn’t actually load their information in the Profile
screen.
Navigation 337
The header navigation bar doesn’t currently show anything. We should attempt to show the
current screen name so the user knows which screen they’re on.
Navigation parameters
When building multiple screens in a mobile application, it is common to have screens that depend
on some particular data in order to display the correct information. A good example is the Profile
screen in this application. Everytime a user presses a contact on the Contacts screen, we expect to
see that specific contact’s information on the next screen.
The secondary screen here is not a child of the previous screen, but it still relies on a piece of data.
In these scenarios, we need to be able to pass this data as part of the transition in our navigation
flow. React Navigation lets us attach navigation parameters using the navigate method.
We previously mentioned that the navigation prop allows us to change parameters for a screen
using its setParams method. We can similarly pass parameters to another screen. Let’s take a look
at how we set this up for navigating from the Contacts screen to Profile:
contact-list/3/screens/Contacts.js
renderContact = ({ item }) => {
const { navigation: { navigate } } = this.props;
const {
id, name, avatar, phone,
} = item;
return (
<ContactListItem
name={name}
avatar={avatar}
phone={phone}
onPress={() => navigate('Profile', { contact: item })}
/>
);
};
In the second argument of the navigate method, we pass a single object for our parameters that
contains a contact key with the value being the actual contact item. This means that every time we
press a contact on the Contacts screen, the user is navigated to the Profile screen with the actual
contact being passed as a parameter.
With this, we can simplify our Profile screen component and not load a specific contact every time
the screen is mounted:
Since we are now receiving a contact object through navigation props, we no longer need to fetch a
random contact using fetchRandomContact(). We can simplify our Profile screen and not fire any
API calls when our screen mounts:
Navigation 338
contact-list/3/screens/Profile.js
export default class Profile extends React.Component {
render() {
const { navigation: { state: { params } } } = this.props;
const { contact } = params;
const {
avatar, name, email, phone, cell,
} = contact;
return (
<View style={styles.container}>
<View style={styles.avatarSection}>
<ContactThumbnail avatar={avatar} name={name} phone={phone} />
</View>
<View style={styles.detailsSection}>
<DetailListItem icon="mail" title="Email" subtitle={email} />
<DetailListItem icon="phone" title="Work" subtitle={phone} />
<DetailListItem icon="smartphone" title="Personal" subtitle={cell} />
</View>
</View>
);
}
}
We’ve removed all local state from Profile and the component is now driven by props. We extract
the contact object from the navigation prop and use that to render the contact’s information. If
you try running the app now, navigating to a specific contact will show the correct information.
Although passing in the contact object as a navigation parameter works, we generally want to avoid
this pattern.
So far, we’ve explored how parents and children use props to communicate. If a parent performs a
state update, that update propagates through props down to its children. When the child receives
the updated props, the child re-renders.
What’s different here is that Profile is not a direct child of the Contacts screen. Instead Profile is
receiving this data through navigation parameters. Navigation parameters are set once, at the time
of navigation. So if the state for a contact changes, that state update will not be propagated through
navigation parameters.
Put another way, we’ve pushed a copy of a part of our state into navigation parameters. But we have
no means in place to update that copy when the state changes.
So, we should instead pass the id of a contact as a parameter. Then, Profile can look up the contact
in the list of all contacts. We’ll explore this improvement once we introduce a centralized location
for all our state later in this chapter.
Navigation 339
We’ve built the first two screens that make up our first navigator. We covered how to transition
between them by using the API supplied by the navigation prop.
Each screen will usually have its own unique set of features, and we’ll sometimes need to be able to
modify how our navigation-specific components are displayed. Next, we’ll explore how to configure
our navigation screen options before moving on to expanding the number of screens and navigation
patterns in our application.
Navigation screen options
With React Navigation, we can use the navigationOptions property to modify navigation set-
tings for a particular screen or to modify settings for every screen within a navigator (such as
StackNavigator).
We’ll use this property to add a title to the navigation header at the top of both screens. Let’s add it
to each of our screens in route.js:
contact-list/3/routes.js
export default StackNavigator({
Contacts: {
screen: Contacts,
navigationOptions: {
title: 'Contacts',
},
},
We add our options using a navigationOptions object. We specify the title attribute to be Contacts.
We can run the application to confirm that this works.
Navigation 340
Contacts
Now let’s do the same for the Profile screen along with adding a few more details:
contact-list/3/routes.js
Profile: {
screen: Profile,
navigationOptions: ({ navigation: { state: { params } } }) => {
const { contact: { name } } = params;
return {
title: name.split(' ')[0],
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
};
},
},
Although we passed in an object to navigationOptions for the Contacts screen, we also can pass in
a function. Passing a function gives us access to the navigation prop. This is useful when we want
Navigation 341
our options to be derived from the navigation parameters. Here, we get the name of our contact and
use the split
77
method to only render her or his first name as the title
We also modify the colors of our header by using headerTintColor, which allows us to change
the text color, and headerStyle to pass in an object of styles for the header. We change the
backgroundColor to blue.
If we try navigating to the Profile screen now, we’ll see our header styled appropriately.
Profile
Notice on iOS that the title on the left of the header defaults to the title of the previous
screen. We have control over this with the
headerBackTitle
property of navigation options
if we need to modify this.
For a full list of navigator configuration options provided by StackNavigator, refer to the
documentation
78
.
77
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
78
https://reactnavigation.org/docs/navigators/stack
Navigation 342
Default Navigation Options
Although we can specify all of our navigation options for each screen separately, it may
be useful to set default navigation options for multiple screens if they all share the same
configurations. We can do this by defining navigationOptions at the level of the navigator.
For example:
export default StackNavigator({
Contacts: {
screen: Contacts,
navigationOptions: {
headerStyle: {
backgroundColor: 'white',
},
},
},
Profile: {
screen: Profile,
},
}, {
navigationOptions: {
headerStyle: {
backgroundColor: colors.blue,
},
},
});
Screen-specific navigation options for the same configuration will overwrite those defined
for the navigator. In this example, all the screens within this navigator will have a default
background color of blue for their header components. The Contacts screen however will
have a white background color that overwrites the default setting.
Although we can add all of our screen-specific navigation options where we define our navigators
in routes.js, that file can quickly become quite large if we have a lot of screens and configuration
settings. We can instead define each screen’s navigation options inside each component.
Let’s begin with Contacts. We’ll remove the navigationOptions property from our routes.js file
and add it as a static class method to the Contacts component:
Navigation 343
contact-list/4/screens/Contacts.js
export default class Contacts extends React.Component {
static navigationOptions = {
title: 'Contacts',
};
state = {
This is the same technique we’ve used previously for propTypes and defaultProps. We can now do
the same thing for Profile:
contact-list/4/screens/Profile.js
export default class Profile extends React.Component {
static navigationOptions = ({ navigation: { state: { params } } }) => {
const { contact: { name } } = params;
return {
title: name.split(' ')[0],
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
};
};
render() {
Without the navigationOptions properties, we can clean up our routes.js file:
contact-list/4/routes.js
1 import { StackNavigator } from 'react-navigation';
2
3 import Contacts from './screens/Contacts';
4 import Profile from './screens/Profile';
5
6 export default StackNavigator(
7 {
8 Contacts: {
9 screen: Contacts,
10 },
11 Profile: {
12 screen: Profile,
Navigation 344
13 },
14 },
15 {
16 initialRouteName: 'Contacts',
17 },
18 );
The only new thing we’ve added here is the initialRouteName property. Without this property,
the first screen listed is the default screen. However, it’s better to explicitly define the initial route,
since we generally shouldn’t rely on the implicit order of keys within an object. Now, wherever this
navigator is loaded in the application, the first screen that shows will be the Contacts screen.
Like initialRouteName, React Navigation allows us to modify a number of different con-
figuration properties for stack navigators besides navigationOptions. The documentation
79
goes into more detail on each of them.
Tab navigation
A single stack navigator might suffice for a small mobile app with just two or three screens. However,
most applications have more than a few screens and only using stack navigation may not be the most
efficient way to navigate throughout the entire app. This is where another navigation paradigm, like
tabs, can be useful.
We can use tab navigation to allow the user to navigate to a number of different screens at the root
level. Tabs are suitable when a number of screens carry roughly equal importance.
Bottom navigation - (from Material Design documentation - Components)
79
https://reactnavigation.org/docs/stack-navigator.html#stacknavigatorconfig
Navigation 345
Favorites
Before we begin adding tab navigation components to our application, we’ll need to build a few more
screens. Let’s build the Favorites screen of our application first. We can create a Favorites.js file
within the screens directory and begin with its imports:
contact-list/5/screens/Favorites.js
import React from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
} from 'react-native';
import { fetchContacts } from '../utils/api';
import ContactThumbnail from '../components/ContactThumbnail';
As we briefly described earlier in this chapter, this screen will be responsible for showing a list of
favorited contacts.
The Favorites component will not be a child of the Contacts component. And because we’ll be using
tab navigation as opposed to stack navigation, we can’t pass contacts to favorites via a navigation
prop.
Therefore, we’ll use the fetchContacts() function to fetch this data directly from the API. We
usually want to avoid making numerous API calls to obtain the same data on every screen. Once
we include a state container later in the chapter, we’ll remove this.
Note that we’ve set up the fetchContacts API call to randomly “favorite” contacts by setting the
favorites boolean on the contact object to true. That way, we should always have some contacts
displayed on this screen.
Navigation 346
contact-list/5/screens/Favorites.js
export default class Favorites extends React.Component {
static navigationOptions = {
title: 'Favorites',
};
state = {
contacts: [],
loading: true,
error: false,
};
async componentDidMount() {
try {
const contacts = await fetchContacts();
this.setState({
contacts,
loading: false,
error: false,
});
} catch (e) {
this.setState({
loading: false,
error: true,
});
}
}
The screen’s render function:
contact-list/5/screens/Favorites.js
renderFavoriteThumbnail = ({ item }) => {
const { navigation: { navigate } } = this.props;
const { avatar } = item;
return (
<ContactThumbnail
avatar={avatar}
onPress={() => navigate('Profile', { contact: item })}
/>
);
Navigation 347
};
render() {
const { loading, contacts, error } = this.state;
const favorites = contacts.filter(contact => contact.favorite);
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading &&
!error && (
<FlatList
data={favorites}
keyExtractor={keyExtractor}
numColumns={3}
contentContainerStyle={styles.list}
renderItem={this.renderFavoriteThumbnail}
/>
)}
</View>
);
}
In render, we filter our list of contacts using a favorites flag. Using the same pattern we used in
Contacts, we show a loading indicator while the request is still being made, an error message if the
request fails, or the list of contacts.
We’re making use of the numColumns prop for FlatList to render three contacts in every row.
The renderFavoriteThumbnail method is responsible for every item in the list where we use our
ContactThumbnail component to display the user’s avatar. Notice how we’ve also passed in a
navigate action to the onPress prop. This allows the user to navigate to the contact’s profile screen
by pressing an avatar on the Favorites screen - just like when they press a contact on the Contacts
screen.
Since we’re using FlatList again, we can hook up a keyExtractor method once more:
Navigation 348
contact-list/5/screens/Favorites.js
import ContactThumbnail from '../components/ContactThumbnail';
const keyExtractor = ({ phone }) => phone;
export default class Favorites extends React.Component {
And we can finish things off here by adding a few styles:
contact-list/5/screens/Favorites.js
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
justifyContent: 'center',
flex: 1,
},
list: {
alignItems: 'center',
},
});
Try it out
In a moment, we’ll build the Users screen then add tab navigation to our app. Before we do, let’s
add Favorites to our stack navigator just so we can see what it looks like. We’ll set it as our initial
route:
contact-list/routes.js
import { StackNavigator } from 'react-navigation';
import Contacts from './screens/Contacts';
import Profile from './screens/Profile';
import Favorites from './screens/Favorites';
export default StackNavigator(
{
Contacts: {
screen: Contacts,
},
Profile: {
screen: Profile,
Navigation 349
},
Favorites: {
screen: Favorites,
},
},
{
initialRouteName: 'Favorites',
},
);
Favorites
Note that pressing on any avatar navigates to the Profile screen.
As Favorites will not be a part of our stack navigation, go ahead and revert the changes here to the
original route configurations.
User screen
Let’s now build the third root screen, User, which displays the details of the user of the app. We’ll
begin with its imports:
Navigation 350
contact-list/5/screens/User.js
import React from 'react';
import { StyleSheet, Text, View, ActivityIndicator } from 'react-native';
import ContactThumbnail from '../components/ContactThumbnail';
import colors from '../utils/colors';
import { fetchUserContact } from '../utils/api';
We’re using another method, fetchUserContact, from our API utility file. This fetches a single
contact.
We define the header styles at the top of the component:
contact-list/5/screens/User.js
export default class User extends React.Component {
static navigationOptions = {
title: 'Me',
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
};
Similar to the Profile screen, we’re displaying a blue header bar with white text.
Our local state and componentDidMount method will follow the same pattern as other screens:
contact-list/5/screens/User.js
state = {
user: [],
loading: true,
error: false,
};
async componentDidMount() {
try {
const user = await fetchUserContact();
this.setState({
user,
loading: false,
error: false,
Navigation 351
});
} catch (e) {
this.setState({
loading: false,
error: true,
});
}
}
Our render method will show the ContactThumbnail with the user’s name, avatar and phone
number:
contact-list/5/screens/User.js
render() {
const { loading, user, error } = this.state;
const { avatar, name, phone } = user;
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading && (
<ContactThumbnail avatar={avatar} name={name} phone={phone} />
)}
</View>
);
}
And finally, we’ll style our container to have a blue background as well as place our thumbnail in
the center of our screen:
contact-list/5/screens/User.js
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.blue,
},
});
Navigation 352
Try it out
We’ll again temporarily modify our routes.js file to test out this component:
contact-list/routes.js
import { StackNavigator } from 'react-navigation';
import Contacts from './screens/Contacts';
import Profile from './screens/Profile';
import User from './screens/User';
export default StackNavigator(
{
Contacts: {
screen: Contacts,
},
Profile: {
screen: Profile,
},
User: {
screen: User,
},
},
{
initialRouteName: 'User',
},
);
Navigation 353
User
Nested navigators
Now that we have all the screens that make up our tabs, we can start putting our tab navigation
logic in place. Let’s import and use the TabNavigator component in our routes.js file:
contact-list/routes.js
import { TabNavigator } from 'react-navigation';
import Contacts from './screens/Contacts';
import Favorites from './screens/Favorites';
import User from './screens/User';
export default TabNavigator({
Contacts: {
screen: Contacts,
},
Favorites: {
screen: Favorites,
},
Navigation 354
User: {
screen: User,
},
});
If we run the app, we’ll see tabs with labels at the bottom of the iOS device screen and at the top of
the Android screen. These tabs allow us to switch between the three screens.
Tabs
We’ll modify the styling of our tabs in a little bit. At the moment, we have a bigger problem. We
don’t have our header component anymore and if we try pressing any of the contacts in the Contacts
or Favorites screens, nothing happens. This is because we’ve removed our StackNavigator entirely
and only have a single tab navigation system in place. What we want to do is compose our two
navigators.
Let’s update our route configurations. We’ll begin with the imports since we’ll be including a few
new ones:
Navigation 355
contact-list/5/routes.js
import React from 'react';
import { StackNavigator, TabNavigator } from 'react-navigation';
import { MaterialIcons } from '@expo/vector-icons';
import Favorites from './screens/Favorites';
import Contacts from './screens/Contacts';
import Profile from './screens/Profile';
import User from './screens/User';
import colors from './utils/colors';
We’re importing both navigators from react-navigation as well as MaterialIcons from Expo’s
vector-icons package. This package is a wrapper around react-native-vector-icons
80
, a library
that contains a number of vector icons. Creating icons is done by using JSX to define icon
components. For this reason, we’ve imported React to this file as well.
You can find a list of all the icons provided by the library here
81
. There are a number
of different icon sets that can be used (such as FontAwesome). We’ll only be using the
MaterialIcons icon set for this application.
To compose stack navigation and tab navigation, each tab will have its own separate navigation
stack. This means that instead of passing specific screens to each tab, we’ll pass in a stack (a
StackNavigator component) that contains every possible screen within that tab.
For example, we want the contacts list to still use a stack navigator. That way, the user can navigate
to individual profiles. That stack navigator will reside inside our app’s broader tab navigator.
Let’s write our stack navigator for the contacts list. It looks the same as before, with one additional
configuration:
contact-list/5/routes.js
const ContactsScreens = StackNavigator(
{
Contacts: {
screen: Contacts,
},
Profile: {
screen: Profile,
},
},
80
https://github.com/oblador/react-native-vector-icons
81
https://expo.github.io/vector-icons/
Navigation 356
{
initialRouteName: 'Contacts',
navigationOptions: {
tabBarIcon: getTabBarIcon('list'),
},
},
);
For the “Contacts” tab, we want to first show the Contacts screen and allow the user to be able to
navigate to the Profile screen.
Notice how we’ve also added navigation options to specify this navigator’s tabBarIcon. We’re
passing in a getTabIcon helper method to retrieve a list icon component. Let’s define this function
right after our imports:
contact-list/5/routes.js
import colors from './utils/colors';
const getTabBarIcon = icon => ({ tintColor }) => (
<MaterialIcons name={icon} size={26} style={{ color: tintColor }} />
);
const ContactsScreens = StackNavigator(
The tabBarIcon option expects a function. It will call that function with a single object that has the
properties focused and tintColor.
The getTabIcon function returns a function that returns a specific icon component from the
MaterialIcons icon set given its name. When we define our TabNavigator component, we’ll assign
the tint colors for all icons in each of our tabs.
Higher-order functions
The tabBarIcon parameter in our navigation options expects a function that takes an object with
focused and tintColor as attributes. It looks like the following:
tabBarIcon: (params) => (
<MaterialIcons
name="list"
size={26}
style={{ color: params.tintColor }}
/>
),
Navigation 357
The focused argument allows us to render something different depending on whether the current
tab is focused in view or not. We’re not doing anything for the different states so we do not even
use focused at all. We only use tintColor in order to give our tab icons an appropriate active or
inactive color defined as options in TabNavigator.
With parameter context matching
a
, we can simplify how we pass in our arguments:
tabBarIcon: ({ tintColor }) => (
<MaterialIcons
name="list"
size={26}
style={{ color: tintColor }}
/>
),
Since every icon in each of the tabs have the same tint color and size, we simplify how we render
our icons by defining a single getTabIcon function:
const getTabBarIcon = icon => ({ tintColor }) => (
<MaterialIcons name={icon} size={26} style={{ color: tintColor }} />
);
// ...
tabBarIcon: getTabBarIcon('list'),
The getTabIcon function takes an icon string as a parameter and returns a function that takes the
correct parameters expected by tabBarIcon (an object with tintColor and focused).
a
appendix_higher_order_components
Let’s do the same for our other two tabs, Favorites and User:
contact-list/5/routes.js
const FavoritesScreens = StackNavigator(
{
Favorites: {
screen: Favorites,
},
Profile: {
screen: Profile,
},
},
{
initialRouteName: 'Favorites',
Navigation 358
navigationOptions: {
tabBarIcon: getTabBarIcon('star'),
},
},
);
const UserScreens = StackNavigator(
{
User: {
screen: User,
},
},
{
initialRouteName: 'User',
navigationOptions: {
tabBarIcon: getTabBarIcon('person'),
},
},
);
For the Favorites tab, the idea is similar. We want to default to the Favorites screen but still allow
the user to navigate to the Profile screen for any specific contact. The User stack navigator only
has one screen at the moment, but we’ll add a second screen a little later.
Now that we’ve defined our stack navigators, we can write up our tab navigator underneath:
contact-list/5/routes.js
export default TabNavigator(
{
Contacts: {
screen: ContactsScreens,
},
Favorites: {
screen: FavoritesScreens,
},
User: {
screen: UserScreens,
},
},
{
initialRouteName: 'Contacts',
tabBarPosition: 'bottom',
tabBarOptions: {
Navigation 359
style: {
backgroundColor: colors.greyLight,
},
showLabel: false,
showIcon: true,
activeTintColor: colors.blue,
inactiveTintColor: colors.greyDark,
renderIndicator: () => null,
},
},
);
We pass each StackNavigator as the screen for their corresponding tab. React Navigation allows us
to pass entire navigators as a tab screen and the first screen of the stack will be the default screen
for that tab. We’ve also defined some configurations for our tabs:
tabBarPosition allows us to have our tabs at the top or bottom of our screen. For iOS, this
defaults to bottom and Android defaults to top. Specifying bottom means the tabs will render
at the bottom for both platforms.
tabBarOptions allow us to modify styling for our tabs. We first define the tab background color
using the style object. Since we only want icons to show, we’ve also specified showLabel to be
false and showIcon to true. We set our icon colors for both active (where the user is currently
viewing) and inactive tabs. The last option, renderIndicator, allows us to pass in a function
to modify how tab indicators (or lines at the bottom of the active tab) are rendered. A default
tab indicator shows for Android and we pass null to remove it entirely.
With regards to tabBarOptions, there are options specific to Android that do not apply
to iOS. Every option we’ve specified so far is customizable for both platforms. For more
information on tab configurations and options, refer to the documentation
82
.
Try it out
If we try running the application now, we’ll see our complete tab logic working!
82
https://reactnavigation.org/docs/navigators/tab
Navigation 360
Contacts
Try navigating between the different tabs as well as pressing a contact list item or thumbnail to
navigate to the Profile screen. You’ll notice that navigating to the Profile screen in one tab will
only show that screen there. Composing both tab and stack navigation allows for more complex
navigation architectures where each stack in a tab maintains the history of its own navigated screens
independently from the others.
Modal navigation
We’ll introduce our final screen in the app, Options, to demonstrate how stack navigators can be
modified to render new screens with a modal. This is only for iOS and does not work for Android.
We can begin by creating an Options.js file in the screens/ directory:
Navigation 361
contact-list/screens/Options.js
1 import React from 'react';
2 import { StyleSheet, View } from 'react-native';
3 import { MaterialIcons } from '@expo/vector-icons';
4
5 import DetailListItem from '../components/DetailListItem';
6 import colors from '../utils/colors';
7
8 export default class Options extends React.Component {
9 static navigationOptions = ({ navigation: { goBack } }) => ({
10 title: 'Options',
11 headerLeft: (
12 <MaterialIcons
13 name="close"
14 size={24}
15 style={{ color: colors.black, marginLeft: 10 }}
16 onPress={() => goBack()}
17 />
18 ),
19 });
20
21 render() {
22 return (
23 <View style={styles.container}>
24 <DetailListItem title="Update Profile" />
25 <DetailListItem title="Change Language" />
26 <DetailListItem title="Sign Out" />
27 </View>
28 );
29 }
30 }
31
32 const styles = StyleSheet.create({
33 container: {
34 flex: 1,
35 backgroundColor: 'white',
36 },
37 });
In this screen, we render a few DetailListItem components to represent options that the user
can press to modify their profile settings. We’ve included a headerLeft attribute for the screen’s
Navigation 362
navigation options that renders a close icon. We’ve added a callback to the icon’s onPress prop that
fires navigate.goBack to close the current screen and return to the previous one.
Let’s build the functionality to allow the user to navigate to this screen. We’ll do this in the User
screen by adding an icon to our header:
contact-list/screens/User.js
static navigationOptions = ({ navigation: { navigate } }) => ({
title: 'Me',
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
headerRight: (
<MaterialIcons
name="settings"
size={24}
style={{ color: 'white', marginRight: 10 }}
onPress={() => navigate('Options')}
/>
),
});
Don’t forget to import MaterialIcons in this file.
Now, in routes.js, we’ll import our Options screen:
contact-list/routes.js
import Options from './screens/Options';
Then add it to the UserScreens stack navigator within routes.js:
contact-list/routes.js
const UserScreens = StackNavigator(
{
User: {
screen: User,
},
Options: {
screen: Options,
},
},
{
Navigation 363
mode: 'modal',
initialRouteName: 'User',
navigationOptions: {
tabBarIcon: getTabBarIcon('person'),
},
},
);
We use the mode attribute to specify this navigator should have modal transitions for iOS.
Try it out
Start the app and give it a shot! You’ll be able to navigate directly to the Options screen through
User.
User
By pressing the icon on the right of our header bar, you’ll notice that the screen moves up from the
bottom if you own an iOS device.
Navigation 364
Options
If you try on Android device or emulator, the screen will fade in just like any of the other screens
in the stack.
Drawer navigation
Another navigation pattern that is commonly used is drawer navigation, where views are accessible
through a drawer that slides in from the left side of the screen.
Navigation 365
Navigation drawer - (from Material Design documentation - Components)
Just like tab navigation, a drawer navigator allows users to switch between equally important views
quickly.
Neither of these patterns is better than the other. Choosing the right pattern depends on both
preferences as well as the number of root screens that the user can access.
A good rule of thumb is that tabs work well when there are three to five of them. If there are many
important, unrelated screens that the user should be able to access without navigating through a
stack, then drawer navigation may be more suitable.
Although our final app only includes tab navigation for our three core views, let’s explore what
using drawer navigation would look like. We’ll swap in DrawerNavigator for TabNavigator.
Let’s modify our routes.js file:
contact-list/6/routes.js
1 import React from 'react';
2 import { StackNavigator, DrawerNavigator } from 'react-navigation';
3 import { MaterialIcons } from '@expo/vector-icons';
4
5 import Favorites from './screens/Favorites';
6 import Contacts from './screens/Contacts';
7 import Profile from './screens/Profile';
8 import User from './screens/User';
9 import Options from './screens/Options';
10
11 const getDrawerItemIcon = icon => ({ tintColor }) => (
Navigation 366
12 <MaterialIcons name={icon} size={22} style={{ color: tintColor }} />
13 );
14
15 const ContactsScreens = StackNavigator(
16 {
17 Contacts: {
18 screen: Contacts,
19 },
20 Profile: {
21 screen: Profile,
22 },
23 },
24 {
25 initialRouteName: 'Contacts',
26 navigationOptions: {
27 drawerIcon: getDrawerItemIcon('list'),
28 },
29 },
30 );
31
32 const FavoritesScreens = StackNavigator(
33 {
34 Favorites: {
35 screen: Favorites,
36 },
37 Profile: {
38 screen: Profile,
39 },
40 },
41 {
42 initialRouteName: 'Favorites',
43 navigationOptions: {
44 drawerIcon: getDrawerItemIcon('star'),
45 },
46 },
47 );
48
49 const UserScreens = StackNavigator(
50 {
51 User: {
52 screen: User,
53 },
54 Options: {
Navigation 367
55 screen: Options,
56 },
57 },
58 {
59 mode: 'modal',
60 initialRouteName: 'User',
61 navigationOptions: {
62 drawerIcon: getDrawerItemIcon('person'),
63 },
64 },
65 );
66
67 export default DrawerNavigator(
68 {
69 Contacts: {
70 screen: ContactsScreens,
71 },
72 Favorites: {
73 screen: FavoritesScreens,
74 },
75 User: {
76 screen: UserScreens,
77 },
78 },
79 {
80 initialRouteName: 'Contacts',
81 },
82 );
Note that instead of the tabBarIcon option, we use drawerIcon to display an icon near the menu item
within the drawer. We also change the name of the method that returns our icon from getTabIcon
to getDrawerIcon.
Although the drawer can be accessed by swiping on the left edge of the device screen towards the
right, it also helps to have a menu icon in each of the main screens that can open and close the
drawer. We’ll begin with the Contacts screen:
Navigation 368
contact-list/6/screens/Contacts.js
static navigationOptions = ({ navigation: { navigate } }) => ({
title: 'Contacts',
headerLeft: (
<MaterialIcons
name="menu"
size={24}
style={{ color: colors.black, marginLeft: 10 }}
onPress={() => navigate('DrawerToggle')}
/>
),
});
We’ve added a menu icon on the left hand side of the header. With React Navigation, we can navigate
to DrawerOpen and DrawerClose to open and close the drawer respectively. DrawerToggle will fire
either of those depending on the current state of the navigation drawer. This allows us to toggle the
drawer with a single method.
We can add this same icon to the Favorites screen:
contact-list/6/screens/Favorites.js
static navigationOptions = ({ navigation: { navigate } }) => ({
title: 'Favorites',
headerLeft: (
<MaterialIcons
name="menu"
size={24}
style={{ color: colors.black, marginLeft: 10 }}
onPress={() => navigate('DrawerToggle')}
/>
),
});
And the User screen:
Navigation 369
contact-list/6/screens/User.js
static navigationOptions = ({ navigation: { navigate } }) => ({
title: 'Me',
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
headerLeft: (
<MaterialIcons
name="menu"
size={24}
style={{ color: 'white', marginLeft: 10 }}
onPress={() => navigate('DrawerToggle')}
/>
),
headerRight: (
<MaterialIcons
name="settings"
size={24}
style={{ color: 'white', marginRight: 10 }}
onPress={() => navigate('Options')}
/>
),
});
Try it out
Try running the application with these drawer settings enabled. We can see a menu icon in either
of our three root screens.
Navigation 370
Contacts Screen - Drawer
We can open and close our drawer by swiping right on the left edge of the screen or by pressing the
menu icon.
Navigation 371
Drawer
Sharing state between screens
So far, we’ve built our entire application using local component state. While doing so, we noticed
some of the challenges that applications with multiple screens have when data must be shared
between screens.
For example, our Favorites screen uses the same list of contacts as the Contacts screen. But we had
to make an API call for each screen, fetching the list twice. It would be better to fetch the list just
once and share data between screens.
There are a few different ways we can make this better. One way is to define all the data within
our application in the App component. We can then use the initialRouteParams property that React
Navigation provides for our root tab navigator to assign the state to its initial route - Contacts.
With this approach, we can continue to pass our entire data to every other screen we navigate using
navigation parameters similar to how we pass contact information from Contacts to Profile.
This method of maintaining state is not ideal due to the fact that every screen now has access to all
the data in the entire app. Moreover, this will most likely create performance issues as we would
need to re-render every screen to reflect changes to the state being modified in a specific screen.
Navigation 372
State containers
React Native applications that contain a navigation architecture generally handle data flow differ-
ently than we’ve done in our apps so far. So far, we’ve stored data in the root and screen components
of our apps, and we’ve passed data down from parent to child as props. In this app, we’ll use a
state container to manage all of our application data in a separate external location outside of our
components.
This can be useful to separate the UI and data concerns in our application. In a typical application
with multiple top-level screens, this approach allows us to pass parts of our state to each of our
screens.
Third-party libraries
One approach to including a state container is to use a third-party library. Redux
83
and MobX
84
are
two popular examples that allow users to maintain their entire application state in a single location.
They also impose certain restrictions on how this state object can be modified.
Using a community-supported library means we don’t have to spend the time trying to build the
logic ourselves. There are packages that exist in both the Redux and MobX ecosystem that allow
us to bind our React or React Native components directly to the global store. Modifying our state
requires an action to be dispatched which gives us more explicit control to modify our state in only
specific parts of our application. We can also take advantage of middlewares to intercept any of
our actions before it reaches our state. This can allow us to log all of our state changes for easier
debugging as well as fire asynchronous operations as part of our actions if we need to.
Downsides of using an external library include the learning curve needed to learn its API and specific
requirements. Redux, for example, adheres to the use of pure functions (or functions without any
side effects) and requires a decent amount of boilerplate code in order to connect a component to
our store with appropriate actions. MobX uses the concept of reactive programming and observables
in order to have our state update when our data is changed. Using either of these libraries or another
state management tool altogether means that we would need to fully understand how they work.
Custom state container
Instead of using a community-supported library, we always have the option of building our own
state container in our application. This can be useful when we want complete control over how we
manage our data, or when we don’t want to introduce a complex dependency.
While suitable for our needs in this app, a simple state container built from scratch will not have
nearly as many features or plugins as a third-party library. Most medium and large React Native
applications use Redux or MobX. These libraries have a bit of a learning curve and require more
boilerplate code, but they make things more predictable by constraining how we manage our state.
83
https://github.com/reactjs/redux
84
https://github.com/mobxjs/mobx
Navigation 373
They can help us build consistent applications that are easier to test, debug and analyze using
different built-in or external plugins.
To demonstrate the overall approach of how to manage state in a central location with navigation,
we’ll use an extremely simple state container of our own instead of relying on a third-party solution.
Copy over the the contact-list/store.js file in the sample code to the root of you application. If
you like, take a look at the file to get a better understanding of how it works.
In this file, we’ve defined all of our application state as a single object called state:
contact-list/store.js
1 let state = {
2 isFetchingContacts: true,
3 isFetchingUser: true,
4 contacts: [],
5 user: {},
6 error: false,
7 };
Each of the field values in this object are the default values when our application is first launched.
We then export three methods that can be used throughout our application:
getState returns the application state.
setState takes in new values and updates our state. We don’t mutate the current state object
directly, but instead create a new copy with our updated values.
onChange allows us to listen for changes in our state.
Let’s begin by including it to our Contacts screen. We’ll start with importing our store:
contact-list/7/screens/Contacts.js
import store from '../store';
Now we can make use of our store methods:
Navigation 374
contact-list/7/screens/Contacts.js
export default class Contacts extends React.Component {
static navigationOptions = {
title: 'Contacts',
};
state = {
contacts: store.getState().contacts,
loading: store.getState().isFetchingContacts,
error: store.getState().error,
};
async componentDidMount() {
this.unsubscribe = store.onChange(() =>
this.setState({
contacts: store.getState().contacts,
loading: store.getState().isFetchingContacts,
error: store.getState().error,
}));
const contacts = await fetchContacts();
store.setState({ contacts, isFetchingContacts: false });
}
componentWillUnmount() {
this.unsubscribe();
}
We’ve set up our local state by connecting its attributes to the correct global store attributes. Notice
how we’ve defined loading to be equal to the global state attribute of isFetchingContacts. This
component does not need to know anything else in the state that it is not using (and this includes
the isFetchingUser boolean that would be used in the User screen). We have complete control of
how we want to refer and define our state relevant to the context of this component.
In componentDidMount, we set our onChange method to update our local component state using
this.setState. When we get the results of our API call, we use our store setState method to
update our shared store and since we also use onChange - our local component state will also update
to reflect this change. We make sure to unsubscribe from our change listener when our component
unmounts as well.
Now in our render method, we’ll need to update which parameters we look for within our state:
Navigation 375
contact-list/7/screens/Contacts.js
render() {
const { contacts, loading, error } = this.state;
const contactsSorted = contacts.sort((a, b) =>
a.name.localeCompare(b.name));
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading &&
!error && (
<FlatList
data={contactsSorted}
keyExtractor={keyExtractor}
renderItem={this.renderContact}
/>
)}
</View>
);
}
Let’s do the same thing for Favorites:
contact-list/7/screens/Favorites.js
export default class Favorites extends React.Component {
static navigationOptions = {
title: 'Favorites',
};
state = {
contacts: store.getState().contacts,
loading: store.getState().isFetchingContacts,
error: store.getState().error,
};
async componentDidMount() {
const { contacts } = this.state;
this.unsubscribe = store.onChange(() =>
this.setState({
Navigation 376
contacts: store.getState().contacts,
loading: store.getState().isFetchingContacts,
error: store.getState().error,
}));
if (contacts.length === 0) {
const fetchedContacts = await fetchContacts();
store.setState({ contacts: fetchedContacts, isFetchingContacts: false });
}
}
componentWillUnmount() {
this.unsubscribe();
}
We technically shouldn’t need to submit fetch our contacts from the API again. We know that when
the user loads the application for the first time, the contacts are retrieved within the Contacts screen,
which is the first tab. However, it’s generally better not rely on the order each screen loads to ensure
our data is ready. This is both for testability (we can easily run this screen individually), and because
if we later add the capability to navigate to this screen without loading the Contacts screen first,
our application will fail. A good example of when this might happen is when we allow a user to
deep link to a specific screen from outside the application entirely. We’ll explore this concept in a
bit.
Similarly, we can update our render method as well:
contact-list/7/screens/Favorites.js
render() {
const { contacts, loading, error } = this.state;
const favorites = contacts.filter(contact => contact.favorite);
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading &&
!error && (
<FlatList
data={favorites}
keyExtractor={keyExtractor}
numColumns={3}
Navigation 377
contentContainerStyle={styles.list}
renderItem={this.renderFavoriteThumbnail}
/>
)}
</View>
);
}
And finally, we can update our User screen to follow this same approach:
contact-list/7/screens/User.js
export default class User extends React.Component {
static navigationOptions = {
title: 'Me',
headerTintColor: 'white',
headerStyle: {
backgroundColor: colors.blue,
},
};
state = {
user: store.getState().user,
loading: store.getState().isFetchingUser,
error: store.getState().error,
};
async componentDidMount() {
this.unsubscribe = store.onChange(() =>
this.setState({
user: store.getState().user,
loading: store.getState().isFetchingUser,
error: store.getState().error,
}));
const user = await fetchUserContact();
store.setState({ user, isFetchingUser: false });
}
componentWillUnmount() {
this.unsubscribe();
}
Navigation 378
render() {
const { user, loading, error } = this.state;
const { avatar, name, phone } = user;
return (
<View style={styles.container}>
{loading && <ActivityIndicator size="large" />}
{error && <Text>Error...</Text>}
{!loading && (
<ContactThumbnail avatar={avatar} name={name} phone={phone} />
)}
</View>
);
}
}
Don’t forget to import store within Favorites and User!
Try it out
If we try running the application at this point, we’ll notice everything works exactly the same. Again,
the difference now is that each top level screen in our application uses the same shared data object
in our application.
Instead of being more specific about which parts of our central store we wanted to connect to in
each screen, we could have just connected the entire store using state = store.getState();.
However, connecting only parts of the global state to our component not only improves how we
encapsulate which state parameters we need, but it can also improve re-render performance. In
React Native, a component re-renders if any part of its state changes. With this approach, we can
have our component re-render only when the state specific to it changes.
Although a centralized store allowed us to handle how data is managed across a number of top-level
screens, it’s important to remember that this adds an extra layer of abstraction to our application.
Not only do we now pass presentational data from parent components to child components, we
also pass data through navigation parameters when we navigate through certain screens and use a
top-level state container that manages all the data in our application.
Deep Linking
The last major topic we’ll explore in this chapter is deep linking. Deep linking means launching
the app and navigating to a specific screen automatically. A deep link bypasses the tab, stack, and
Navigation 379
drawer navigation, taking the user directly to the desired screen. This can be useful when launching
your app from a webpage, a push notification, or another app.
Imagine the user gets a push notification that a new contact has been added. When the user taps the
notification, they should be taken directly to the profile screen for that contact, rather than having
to navigate their way from the initial screen of the app.
Deep links are similar to typing a URI (Uniform Resource Identifier) into a web browser. On the web,
https://www.fullstackreact.com might load the homepage for the website, while https://www.fullstackreact.com/react-native
will load a different page. Similarly in mobile applications, we perform a deep link using a URI, where
each URI generally takes us to a different screen.
Using a navigation library for a mobile app helps us build our app in terms of screens this makes
it easy to connect any screen of our app to a specific URI.
Let’s explore how we can add deep linking to our current application by allowing the user to navigate
to a specific contact’s profile directly. With Expo, the base URI is different based on the state of the
application:
exp://localhost:19000/+ or exp://10.2.8.358:19000/+ is the URI we use during develop-
ment. 10.2.8.358 is our IP address and 19000 is the port that the app is running. We can see
our IP address and the port right underneath the QR code printed to the terminal when we
start the application with yarn start.
exp://exp.host/@fullstackio/contact-list/+ is what we can use if our app is published to
the Expo client. If you have the final version of this app installed on your device through the
client, try typing this address into a web browser on your mobile device and you’ll navigate
directly to it.
For standalone apps published outside of Expo, the URI can be defined in app.json. For
example:
app.json
{
"expo": {
"scheme": "contact-list"
}
}
This will give us a URI of contact-list://+.
Instead of having to take care of all of these different possible URI values, Expo provides us with a
linkingUri attribute from a Constants object that will resolve to the correct URI depending on the
state of the application.
Now let’s begin adding deep linking to our contact list application! Our goal is to allow a user to navi-
gate to a specific contact only using his or her first name. For example, exp://{linkingUri}/+?name=ali
Navigation 380
will navigate directly to Ali’s profile. If the contact doesn’t exist, we’ll have the user remain in the
Contacts screen and not be navigated anywhere. For a real production application however, it would
make more sense to show a user-friendly error message if this happens.
We’ll begin with importing Linking and Constants into our Contacts screen:
contact-list/screens/Contacts.js
import React from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
Linking,
} from 'react-native';
The Linking API provides methods that allow us to handle incoming deep links as well as open
external links. We’ll add two of these methods to the lifecycle hook that fires after our component
mounts:
contact-list/screens/Contacts.js
async componentDidMount() {
this.unsubscribe = store.onChange(() =>
this.setState({
contacts: store.getState().contacts,
loading: store.getState().isFetchingContacts,
error: store.getState().error,
}));
const contacts = await fetchContacts();
store.setState({ contacts, isFetchingContacts: false });
Linking.addEventListener('url', this.handleOpenUrl);
const url = await Linking.getInitialURL();
this.handleOpenUrl({ url });
}
Let’s go over the two methods we added:
Navigation 381
The getInitialURL method will fire when a URI associated with the app is accessed externally.
This method allows a user to deep link to a particular part of the application when the
app is closed and not running in the background. In here, we pass the URL obtained to a
handleOpenUrl method.
For instances where the app is running in the background, we can listen to URL events and
provide a callback to handle these situations. This is why we use addEventListener and pass
a handler to the same handleOpenUrl method.
Like any event listener, we’ll need to make sure it is removed when our component is destroyed/un-
mounted:
contact-list/screens/Contacts.js
componentWillUnmount() {
Linking.removeEventListener('url', this.handleOpenUrl);
this.unsubscribe();
}
Now that we’ve handled incoming deep links in our component, let’s create our handler method to
respond appropriately to the correct URL:
contact-list/screens/Contacts.js
handleOpenUrl(event) {
const { navigation: { navigate } } = this.props;
const { url } = event;
const params = getURLParams(url);
if (params.name) {
const queriedContact = store
.getState()
.contacts.find(contact =>
contact.name.split(' ')[0].toLowerCase() ===
params.name.toLowerCase());
if (queriedContact) {
navigate('Profile', { id: queriedContact.id });
}
}
}
We use a getURLParams utility function to extract query parameters from a given string and return
an object. For example getURLParams(exp://localhost:19000/+?name=abby) will return {name:
Navigation 382
'abby'}. If the name parameter exists, we check our state for a contact with the same first name
and if so - navigate to the Profile screen for that user.
We’ll also need to import the getURLParams utility function at the top of our file:
contact-list/screens/Contacts.js
import getURLParams from '../utils/getURLParams';
Although deep linking to a contact by name is straightforward, this approach is flawed. If two
contacts have the same name, our logic will fail, always returning the first contact it finds that
meets the condition. Ideally, we would want to pass the contact’s ID as a URI parameter. In this
example application however, we generate random UUIDs for each contact every time we load our
application. For this reason, we’ve shown the name-based approach for simplicity.
Try it out
While developing locally, you can try deep linking with different methods depending on which
platform you’re using:
If you’re using the iOS simulator or an actual device connected to the same network, you can
open Safari and type exp://localhost:19000/+?name=ali into the address bar.
On an Android emulator on the same network, you can test it through a terminal command:
adb shell am start -W -a "android.intent.action.VIEW" -d "exp://localhost:19000/+?na\
me=ali"
If localhost:19000 isn’t working, the application may be running on a different port. Take
a look at the terminal to see which port is being used.
You’ll notice you’ll be navigated directly to his profile screen:
Navigation 383
Deep Linking
If we try navigating to a contact with a name that doesn’t exist, we’ll remain in the Contacts screen.
Summary
Navigation is one of the most crucial elements of building an application that requires multiple
screens. In this chapter, we looked at different navigation patterns as well as how they can be
composed to allow for a complete navigation system. We built out a complete contact list example
app to explore this in-depth, using all of the navigators provided by React Navigation. We then
moved on to building a small state container to further understand how data can be shared between
top-level screens. Finally, we finished the chapter by including deep linking functionality in a specific
part of our application.
While discussing the differences between native and JavaScript navigation libraries, we briefly
touched on how JavaScript implementaions use their own custom components by relying on React
Native components and the Animated API. Animations and gestures are important topics in mobile
development and the next two chapters will dive deeper into how they work in React Native.
Animation
In order to animate a component on the screen, we’ll generally update its size, position, color, or
other style attributes continuously over time. We often use animations to imitate movement in the
real world, or to build interactivity similar to familiar physical objects.
We’ve seen several examples of animation already:
In our weather app, we saw how the KeyboardAvoidingView shrinks to accommodate room for
the keyboard.
In our messaging app, we used the LayoutAnimation API to achieve similar keyboard-related
animations.
In our contacts app, we used react-nagivation, which automatically coordinates transition
animations between screens.
In this chapter and the next, we’ll explore animations in more depth by building a simple puzzle game
app. React Native offers two main animation APIs: Animated and LayoutAnimation. We’ll use these
to create a variety of different animations in our game. Along the way, we’ll learn the advantages
and disadvantages of each approach.
The next chapter (“Gestures”) will primarily focus on a closely related topic: gestures. Gestures help
us build components that respond to tapping, dragging, pinching, rotating, etc. Combining gestures
and animations enables us to build intuitive, interactive experiences in React Native.
Animation challenges
Building beautiful animations can be tricky. Let’s look at a few of the challenges we’ll face, and how
we can overcome them.
Performance challenges
To achieve animations that look smooth, we’ll want our UI to render at 60 frames-per-second (fps).
In other words, we need to render 1 frame roughly every 16 milliseconds (1000 milliseconds / 60
frames). If we perform expensive computations that take longer than 16 milliseconds within a single
frame, our animations may start to look choppy and uneven. Thus, we must constantly pay attention
to performance when working with animation.
Performance issues tend to fall into a few specific categories:
Animation 385
Calculating new layouts during animation: When we change a style attribute that affects
the size or position of a component, React Native usually re-calculates the entire layout of the
UI. This calculation happens on a native thread, but is still an expensive calculation that can
result in choppy animations. In this chapter, we’ll learn how we can avoid this by animating
the transform style attribute of a component.
Re-rendering components: When a component’s state or props change, React must de-
termine how to reconcile these changes and update the UI to reflect them. React is fairly
efficient by default, so components generally render quickly enough that we don’t optimize
their performance. With animation, however, a large component that takes a few milliseconds
to render may lead to choppy animations. In this chapter, we’ll learn how we can reduce re-
renders with shouldComponentUpdate to acheive smoother animations.
Communicating between native code and JavaScript: Since JavaScript runs asynchronously,
JavaScript code won’t start executing in response to a gesture until the frame after the gesture
happens on the native side. If React Native must pass values back and forth between the native
thread and the JavaScript engine, this can lead to slow animations. In this chapter, we’ll learn
how we can use useNativeDriver with our animations to mitigate this.
Complex control flow challenges
When working with animations, we tend to write more asynchronous code than normal. We must
often wait for an animation to complete before starting another animation (using an imperative API)
or unmounting a component. The asynchronous control flow and imperative calls can quickly lead
to confusing, buggy code.
To keep our code clear and accurate, we’ll use a state machine
85
approach for our more complex
components. We’ll define a set of named states for each component, and define transitions from
one state to another. This is similar to the React component lifecycle: our component will transition
through different states (similar to mounting, updating, unmounting, etc), and we can run a specific
function every time the state changes.
If you’re familiar with state machines, you might be wondering how our state machine
approach will differ from normal usage of React component state. React state is an implicit
state machine, which we often use without defining specific states. In our case, we’re going to
be explicit about our states, naming them and defining transitions between them. Ultimately
though, we’re just using React state in a slightly more structured way than normal.
If you’re not familiar with state machines, that’s fine. We’ll be building ours together as we
go through the chapter. Even if you’re not familiar with the term “state machine, the coding
style will probably look familiar, since we’ve used it elsewhere in this book already (e.g. the
INPUT_METHOD from the “Core APIs” chapter).
85
https://en.wikipedia.org/wiki/Finite-state_machine
Animation 386
Building a puzzle game
In this chapter, we’ll learn how to use the React Native animation APIs to build a slider-puzzle game.
You can try the completed app on your phone by scanning this QR code from within the Expo app:
Our app will have two screens. The first screen let’s us choose the size of the puzzle and start a new
game:
Animation 387
The second screen opens when we start the game. The goal of the game is to rearrange the squares
of the puzzle to complete the image displayed in the top left:
Project setup
In this chapter, we’ll work out of the sample code directory for the project. Throughout this book
we’ve already set up several projects and created many components from scratch, so for this project
the import statements, propTypes, defaultProps, and styles are already written for you. We’ll be
adding the animations and interactivity to these components as we go.
We’ll use the contents of puzzle/1 as a foundation for our app. First, you’ll need to copy the
puzzle/1 directory to somewhere else on your computer. Then in the terminal, you can navigate to
the directory you copied and install the dependencies by running yarn.
For example, if you unzipped the book’s sample code to /Downloads/fsrn/, on a macOS or Linux
computer you would run:
$ cp -r ~/Downloads/fsrn/puzzle/1 ~/Downloads/puzzle
$ cd ~/Downloads/puzzle
$ yarn
Animation 388
You won’t be able to work out of the original directory, since it’s nested within another React
Native app directory, and the packager currently doesn’t support nested directories like this.
That’s why you’ll need to copy puzzle/1 elsewhere.
Once this finishes, choose one of the following to start the app:
yarn start - Start the Packager and display a QR code to open the app on your phone
yarn ios - Start the Packager and launch the app on the iOS simulator
yarn android - Start the Packager and launch the app on the Android emulator
You should see a dark full-screen gradient (it’s subtle), which looks like this:
Project Structure
Let’s take a look at the files in the directory we copied:
Animation 389
├── App.js
├── README.md
├── app.json
├── assets
├── logo.png
├── logo@2x.png
└── logo@3x.png
├── components
├── Board.js
├── Button.js
├── Draggable.js
├── Logo.js
├── Preview.js
├── Stats.js
└── Toggle.js
├── package.json
├── screens
├── Game.js
└── Start.js
├── utils
├── api.js
├── clamp.js
├── configureTransition.js
├── controlFlow.js
├── formatElapsedTime.js
├── grid.js
├── puzzle.js
└── sleep.js
├── validators
└── PuzzlePropType.js
└── yarn.lock
Here’s a quick overview of the most important parts:
The App.js file is the entry point of our code, as with our other apps.
The assets directory contains a logo for our puzzle app.
The components directory contains all the component files we’ll use in this chapter. Some of
them have been written already, while others are scaffolds that need to be filled out.
The screens directory contains the two screen components in our app: the Start screen and
the Game screen. The App coordinates the transitions between these two screens.
The utils directory contains a variety of utility functions that let us build a complex app like
this more easily. Most of these functions aren’t specific to React Native, so you can think of
Animation 390
them as a “black box” we’ll cover the relevant APIs, but the implementation details aren’t
too important to understand.
The validators directory contains a custom propTypes function that we’ll use in several
different places.
Now that we’re familiar with the project structure, let’s dive into the code!
App
Let’s walk through how the App component coordinates different parts of the app. Open up App.js.
App state
App stores the state of the current game and renders either the Start screen or the Game screen. A
“game in our app is represented by the state of the puzzle and the specific image used for the puzzle.
To start a new game, the app generates a new puzzle state and chooses a new random image.
If we look at the state object, we can see there are 3 fields:
puzzle/1/App.js
state = {
size: 3,
puzzle: null,
image: null,
};
size - The size of the slider puzzle, as an integer. We’ll allow puzzles that are 3x3, 4x4, 5x5,
or 6x6. We’ll allow the user to choose a different size before starting a new game, and we’ll
initialize the new puzzle with the chosen size.
puzzle - Before a game begins or after a game ends, this value is null. If there’s a current game,
this object stores the state of the game’s puzzle. The state of the puzzle should be considered
immutable. The file utils/puzzle.js includes utility functions for interacting with the puzzle
state object, e.g. moving squares on the board (which returns a new object).
image - The image to use in the slider puzzle. We’ll fetch this image prior to starting the game
so that (hopefully) we can fully download it before the game starts. That way we can avoid
showing an ActivityIndicator and delaying the game.
App screens
Our app will contain two screens: Start.js and Game.js. Let’s briefly look at each.
Start screen
Open Start.js. The propTypes have been defined for you:
Animation 391
puzzle/1/screens/Start.js
static propTypes = {
onChangeSize: PropTypes.func.isRequired,
onStartGame: PropTypes.func.isRequired,
size: PropTypes.number.isRequired,
};
When we write the rest of this component, we’ll be building the buttons that allow switching the
size of the puzzle board. We’ll receive the current size as a prop, and call onChangeSize when we
want to update the size in the state of App. We’ll also build a button for starting the game. When
the user presses this button, we’ll call the onStartGame prop so that App knows to instantiate a puzzle
object and transition to the Game screen.
The state object for this component includes a field transitionState:
puzzle/1/screens/Start.js
state = {
transitionState: State.Launching,
};
This transitionState value indicates the current state of our state machine. Each possible state is
defined in an object called State near the top of the file:
puzzle/1/screens/Start.js
const State = {
Launching: 'Launching',
WillTransitionIn: 'WillTransitionIn',
WillTransitionOut: 'WillTransitionOut',
};
This object defines the possible states in our state machine. We’ll set the component’s transitionState
to each of these values as we animate the different views in our component. We’ll then use
transitionState in the render method to determine how to render the component in its current
state.
We define the possible states as constants in State, rather than assigning strings directly
to transitionState, both to avoid small bugs due to typos and to clearly document all the
possible states in one place.
We can see that the Start screen begins in the Launching state, since transitionState is initialized
to State.Launching. The Start component will transition from Launching when the app starts, to
Animation 392
WillTransitionIn when we’re ready to fade in the UI, to WillTransitionOut when we’re ready to
transition to the Game screen.
We’ll use this pattern of State and transitionState throughout the components in this app to keep
our asynchronous logic clear and explicit.
Game screen
Now open Game.js. Again, the propTypes have been defined for you:
puzzle/1/screens/Game.js
static propTypes = {
puzzle: PuzzlePropType.isRequired,
image: Image.propTypes.source,
onChange: PropTypes.func.isRequired,
onQuit: PropTypes.func.isRequired,
};
The puzzle and image props are used to display the puzzle board. When we want to change the
puzzle, we’ll pass an updated puzzle object to App using the onChange prop. We’ll also present a
button to allow quitting the game. When the user presses this button, we’ll call onQuit, initiating a
transition back to the Start screen.
Like in Start.js, we’ll use a state machine to simplify our code:
puzzle/1/screens/Game.js
const State = {
LoadingImage: 'LoadingImage',
WillTransitionIn: 'WillTransitionIn',
RequestTransitionOut: 'RequestTransitionOut',
WillTransitionOut: 'WillTransitionOut',
};
We’ll cover these states in more detail when we build the screen.
Now that you have an overview of how the state and screens of our app will work, we can dive in
to building animations!
Building the Start screen
In order to build the Start screen, we’ll use the two main building blocks of animation: LayoutAnimation
and Animated. Each of these come with their own strengths and weaknesses. With LayoutAnimation
we can easily transition our entire UI, while Animated gives us more precise control over individual
values we want to animate.
Animation 393
Initial layout
Let’s use LayoutAnimation to animate the position of a logo from the center of the screen to the top
of the screen.
Initially we’ll show this:
Then we’ll animate the position of the logo to turn it into this:
Animation 394
We’re rendering placeholder buttons for now. We’ll style the buttons and add some custom
animations soon.
Open up Start.js. You’ll notice the component’s import statements, propTypes, state, and styles
are already defined.
Let’s start by returning the Logo and a few other components from our render method. Add the
following to render:
puzzle/screens/Start.js
// ...
render() {
const { size, onChangeSize } = this.props;
const { transitionState } = this.state;
return (
<View style={styles.container}>
<View style={styles.logo}>
<Logo />
Animation 395
</View>
<View>
<Toggle options={BOARD_SIZES} value={size} onChange={onChangeSize} />
</View>
<View>
<Button title={'Start Game'} onPress={() => {}} />
</View>
</View>
);
}
// ...
After saving Start.js, you should see the end state of our animation:
We touched on LayoutAnimation in the second half of the “Core APIs” chapter, but let’s revisit how
it works before we use it.
Animation 396
LayoutAnimation
LayoutAnimation automatically animates the entire UI of our application from its current layout
to the next layout. Elements that change position or size will be translated or scaled. Elements
that are added or removed between layouts will be animated too, and we can choose what this
animation looks like. We call LayoutAnimation.create to define an animation configuration, and
then LayoutAnimation.configureNext to enqueue the animation to run the next time render is
called.
The LayoutAnimation.create API takes three parameters:
duration - The duration of the animation
easing - The curve of the animation. We choose from a predefined set of curves: spring, linear,
easeInEaseOut, easeIn, easeOut, keyboard.
creationProp - The style to animate when a new element is added: opacity or scaleXY.
The main advantages of LayoutAnimation are:
We can animate our entire UI with a single function call, rather than starting one or several
animations for each individual component.
We can animate flexbox layout attributes like justifyContent and alignItems, so we don’t
have to specify movement in terms of coordinates.
The API fits nicely with React’s declarative rendering pattern we specify the start and
end states of our UI by returning components from render as we normally would, and
LayoutAnimation figures out the details of how to transition between these states.
The main disadvantages are:
We have limited control over individual animations and individual components, since every
updated layout property of every component animates simultaneously.
We can only animate layout attributes, so if we want to animate other style attributes like color
or opacity we’ll need to use Animated.
Note that we’ve already added the boilerplate code to enable LayoutAnimation on Android
at the top of App.js.
puzzle/screens/Start.js
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
As we mentioned in the Core APIs chapter, this will only be necessary until the
LayoutAnimation implementation on Android matures.
Animation 397
Animating the logo
To animate our logo, we want to initially not render the components below it, then start rendering
them and enqueue an animation with LayoutAnimation.configureNext. Since the outer View
component rendered from our Start component has justifyContent: 'space-around' in its style,
adding more content below the logo will move the logo up.
Our component starts in the Launching state, so let’s update render to only render the View
components below the logo when we’re not in the Launching state:
puzzle/screens/Start.js
// ...
render() {
const { size, onChangeSize } = this.props;
const { transitionState } = this.state;
return (
<View style={styles.container}>
<View style={styles.logo}>
<Logo />
</View>
{transitionState !== State.Launching && (
<View>
<Toggle
options={BOARD_SIZES}
value={size}
onChange={onChangeSize}
/>
</View>
)}
{transitionState !== State.Launching && (
<View>
<Button title={'Start Game'} onPress={() => {}} />
</View>
)}
</View>
);
}
// ...
When the app is in the Launching state, only the logo will appear. If we were to save Start.js now,
our app would render the logo in the center of the screen indefinitely.
Animation 398
When we shift the transitionState from Launching to WillTransitionIn, render() will return the
two View components below the logo. This will push the logo up towards the top of the screen. We
can use LayoutAnimation to animate the logo’s movement and the other two components’ entrances.
Let’s write a componentDidMount method and call LayoutAnimation.configureNext within it and set
our transitionState to WillTransitionIn. We’ll also add a little delay using await and our utility
function sleep so we see the logo in the initial state for a short period of time before starting the
animation.
Add the following to Start.js:
puzzle/screens/Start.js
// ...
async componentDidMount() {
await sleep(500);
const animation = LayoutAnimation.create(
750,
LayoutAnimation.Types.easeInEaseOut,
LayoutAnimation.Properties.opacity,
);
LayoutAnimation.configureNext(animation);
this.setState({ transitionState: State.WillTransitionIn })
}
// ...
After saving Start.js, you should see the animation! The logo should move up toward the top of
the screen, and the “Choose Size” text and placeholder buttons will fade in.
Awaiting LayoutAnimation
Suppose we want to change the timing of the animations so that the buttons don’t fade in until after
the logo finishing moving toward the top of the screen. To do this, we’ll need to know when our
LayoutAnimation completes.
We can use the second parameter of LayoutAnimation.configureNext(animation, completionCallback),
the completionCallback, to wait for an animation to finish. However, as of React Native 0.52, this
callback currently only works on iOS. To overcome this limitation, we’re going to use a utility
function that approximates the completion callback on Android (we’re going to assume that the
animation will complete in the duration we specified for the animation, e.g. 750 milliseconds).
Animation 399
Since we’re going to use the same animation in several different places throughout this app, the
utility function can also encapsulate some of the animation’s configuration parameters. We’ll also
use a promise-based API for our utility function for convenience.
The utility function is a little tricky, so we’ve already written it for you in configureTransition.js:
puzzle/utils/configureTransition.js
import { LayoutAnimation, Platform } from 'react-native';
export default function configureTransition(onConfigured = () => {}) {
const animation = LayoutAnimation.create(
750,
LayoutAnimation.Types.easeInEaseOut,
LayoutAnimation.Properties.opacity,
);
const promise = new Promise(resolve => {
// Workaround for missing LayoutAnimation callback support on Android
if (Platform.OS === 'android') {
LayoutAnimation.configureNext(animation);
setTimeout(resolve, 750);
} else {
LayoutAnimation.configureNext(animation, resolve);
}
});
onConfigured();
return promise;
}
If you don’t follow exactly what happens with the callback argument and promise, that’s
fine. The details of this aren’t important, and the need for this function should go away in a
subsequent React Native release.
We allow passing an onConfigured parameter so that we have a place to easily update component
state with setState between the time we call LayoutAnimation.create and the time we actually
schedule the animation with LayoutAnimation.configureNext.
If we call LayoutAnimation.configureNext before setState, most animations will still work
correctly, but there are a few instances where it won’t work on Android.
Let’s update componentDidMount in Start to use our configureTransition utility function:
Animation 400
puzzle/screens/Start.js
// ...
async componentDidMount() {
await sleep(500);
await configureTransition(() => {
this.setState({ transitionState: State.WillTransitionIn });
});
// ...
}
// ...
When you save the app now, you should see the exact same animation as before. The updated
code is a bit simpler and handles the completion callback, so that’s how we’ll use LayoutAnimation
throughout the rest of the app.
Animating buttons
Now that we can wait for the LayoutAnimation to complete, let’s update the animation of the buttons
to fade in after the logo finishes moving. We could use LayoutAnimation again, but instead we’re
going to learn how to make the same fade animation using the Animated API.
Animated
The Animated API lets us animate most style attributes of core components. While LayoutAnimation
is designed to automatically transition between two states, Animated lets us fine-tune the tim-
ing and duration of individual style attributes. Although Animated gives us more control than
LayoutAnimation, it’s also more complex: we must define which style attributes we want to animate
individually, and imperatively call functions to start these animations.
There are two parts to Animated:
Animated.Value - This is a class that wraps a primitive value (number, string, etc) for use
in component styles. The Animated API includes functions for modifying the primitive value
within the wrapper, e.g. Animated.add.
Animated.createAnimatedComponent(Component) - Components must be specially wrapped
with Animated.createAnimatedComponent in order to handle Animated.Value in their styles.
The Animated API exports a wrapped version of some of the most common components:
Animated.View, Animated.Text, Animated.Image, and Animated.ScrollView. These are just for
convenience Animated.View is equivalent to Animated.createAnimatedComponent(View).
Animation 401
Running animations
It’s time to make the fade animation for our buttons! Let’s begin by instantiating two Animated.Value
instances. One will represent the opacity of the Toggle component, and the other will represent the
opacity of the start Button component. While we could animate both components with a single
Animated.Value, by instead instantiating one for each component, we can animate the components
independently. In our case, we’ll use an animation option called delay to stagger the animations
independently.
We’ll instantiate these animated values as instance properties of our Start component. We’ll add
them below where we instantiate the initial state:
puzzle/screens/Start.js
state = {
transitionState: State.Launching,
};
toggleOpacity = new Animated.Value(0);
buttonOpacity = new Animated.Value(0);
We initialize each Animated.Value with a primitive value of 0. For our animation, this primitive
value will represent that opacity of our components: 0 represents a fully transparent style and 1
represents a fully opaque style.
To perform a fade-in animation, we instruct the Animated.Value to change its primitive value from 0
to 1 over a some duration using an animation curve. There are variety of different animation curves
we can use to update the value:
Animated.timing - This animates a value using an easing curve over a specified duration.
E.g. Animated.timing(this.buttonOpacity, { toValue: 1, duration: 500, easing:
Easing.inOut(Easing.ease) }). The Easing API provides most of the common easing
curve functions used for animation. If we don’t provide a value for easing, the default is
Easing.inOut(Easing.ease), which generally looks pretty good.
Animated.spring - This animates a value using a simulated spring. The tension and friction
of the spring are customizable. E.g. Animated.spring(this.buttonOpacity, { toValue: 1,
friction: 7, tension: 40 }).
Animated.decay - This animates a value by simulating motion and kinetic energy. The ani-
mated value will decay to 0, using the provided velocity and deceleration. E.g. Animated.decay(this.buttonOpacity,
{ velocity: 1, deceleration: 0.997 }).
Both Animated.timing and Animated.spring accept a delay option which will delay when the
animation begins. We’ll use this delay to stagger the animations.
Calling any of these animation functions returns an object with two methods:
Animation 402
start(completionCallback?) - We must always start() an animation when we want it to
run (which is often immediately). We can optionally provide a callback that’s invoked when
the animation completes (or is stopped).
stop() - We can force an animation to stop at any time by calling stop(). If we passed a
completionCallback to the start function, it will be invoked with { finished: false }.
We’ll use Animated.timing for our animations, configuring the animations to last for 500 millisec-
onds. We’ll set the first animation to start 500 milliseconds after the LayoutAnimation finishes, and
we’ll set the second animation to start 1000 milliseconds after.
puzzle/screens/Start.js
async componentDidMount() {
await sleep(500);
await configureTransition(() => {
this.setState({ transitionState: State.WillTransitionIn });
});
Animated.timing(this.toggleOpacity, {
toValue: 1,
duration: 500,
delay: 500,
useNativeDriver: true,
}).start();
Animated.timing(this.buttonOpacity, {
toValue: 1,
duration: 500,
delay: 1000,
useNativeDriver: true,
}).start();
}
Any time we configure an animation, we need to start it with .start(). In this case, we do it right
after calling Animated.timing.
If at any point you run an app and an animation doesn’t work, double check that you’re
calling .start()! It’s very easy to forget.
We’ll come back to useNativeDriver shortly.
Animation 403
Lastly, we need to update the render method to use these animated values. We use animated values
in the style prop of our components, e.g. style={{ opacity: this.buttonOpacity }}, just like we
would if we were using a primitive value directly.
Using an Animated.Value within a component’s style only works if we’re using Animated.View,
Animated.Text, Animated.Image, Animated.ScrollView, or a component created by calling Animated.createAnimatedComponent(Component)
with an existing component. For this reason, we’ll change some of our View components to
Animated.View now that we’re using styles that contain an Animated.Value.
puzzle/screens/Start.js
// ...
render() {
// ...
const toggleStyle = { opacity: this.toggleOpacity };
const buttonStyle = { opacity: this.buttonOpacity };
return (
<View style={styles.container}>
<View style={styles.logo}>
<Logo />
</View>
{transitionState !== State.Launching && (
<Animated.View style={toggleStyle}>
<Toggle
options={BOARD_SIZES}
value={size}
onChange={onChangeSize}
/>
</Animated.View>
)}
{transitionState !== State.Launching && (
<Animated.View style={buttonStyle}>
<Button title={'Start Game'} />
</Animated.View>
)}
</View>
);
}
// ...
Animation 404
Try it out
Save Start.js and reload the app. Now the logo should animate, then the toggle buttons, then finally
the start button at the bottom.
The end state should look the same as it did before:
Animated value performance
These animations should probably look pretty smooth on your device. Let’s briefly discuss how they
work under the hood.
Normally when we want to change how a component is rendered, we change either the props or
state of the component, and this triggers a re-render. However, re-rendering 60 times per second
(to achieve 60fps) is often too slow. Thus, animations must happen outside of the lifecycle of our
components.
You might have noticed that we didn’t store our Animated.Value instances in the state of our Start
component. The Animated.Value is a reference to a class instance that wraps a primitive value.
When we change the underlying primitive value with Animated.timing and start, this doesn’t
change the Animated.Value reference. Even if we were to store the animated value in state, the
component wouldn’t re-render when we call Animated.timing or start.
Animation 405
Animated values can be stored in component state, even though they will never trigger a re-
render. By convention, animated values are either stored in state or as instance properties.
Both are common conventions it’s up to you which you prefer.
When we call Animated.start, the JavaScript animation driver (the thing that actually runs our
animations) uses requestAnimationFrame to update our component’s styles in a way that doesn’t
require a component to re-render (called setNativeProps) every frame.
You may have noticed that we also added useNativeDriver to our animation configurations. This
option is purely for improved animation performance. This option tells React Native to perform
the animation solely on the native thread using the native animation driver, rather than passing
values back and forth between JavaScript and React Native. We can only use useNativeDriver when
animating styles that don’t affect layout, such as opacity, backgroundColor, or transform. We can’t
use useNativeDriver when animating width or top, for example, since these affect layout.
Advanced Animated.Value
Now that we’ve covered the basics of Animated.Value, we can try some more advanced animations.
Let’s replace the placeholder buttons that sit below the logo on the Start screen with some that look
nicer:
Animation 406
So far when we’ve created buttons in this book, we’ve used TouchableOpacity and TouchableHighlight.
The animations for these are pre-defined to change the opacity and background color of the button’s
view. But what if we want a custom button-press animation? In this section, we’ll use the Animated
API to animate the border color, text color, and scale of our own custom Button component.
Updating our buttons
We’re already rendering this Button in our Start screen, and it’s used within the Toggle component,
which we also render from Start. We’ll make the height, color, font size, and border radius of the
button configurable so that we can use the same component in both places.
Open up components/Button.js. Once again, the import statements, propTypes, and styles have
been filled out for you. There’s also a render method that was used to render our placeholder buttons,
but we’ll replace that entirely.
Let’s begin by writing the constructor. Here, we’ll track whether or not the button is currently
pressed within the component’s state. We use the disabled prop to determine if the component is
currently selected (in our toggle component) or not. We’ll also initialize a new Animated.Value to
represent the color and scale of the button, which we’ll store as this.value:
puzzle/components/Button.js
constructor(props) {
super(props);
const { disabled } = props;
this.state = { pressed: false };
this.value = new Animated.Value(getValue(false, disabled));
}
You’ll notice we call a utility function, getValue, to get the initialize the primitive value wrapped
by an Animated.Value. This function has already been written for you at the top of the file:
puzzle/components/Button.js
const getValue = (pressed, disabled) => {
const base = disabled ? 0.5 : 1;
const delta = disabled ? 0.1 : 0.3;
return pressed ? base - delta : base;
};
This utility function returns a different number depending on the state and props of the component.
The exact numbers were chosen arbitrarily to make the animation look nice, but let’s go into a little
more detail about what these will be used for.
Animation 407
When we used Animated.Value for opacity previously (our components below the logo), we used a
value between 0 and 1 to represent the opacity. We were able to use the Animated.Value directly,
since opacity is also a number between 0 and 1. In this case, however, we want to animate a color
value, rather than a number. An Animated.Value always wraps a primitive number value, but we can
use the interpolate method of an animated value to interpolate the number into a different range
(including a range of colors!). For example, we could use 0 to represent black and 1 to represent
white, and interpolate any value between 0 and 1 into a gray color in the range between black and
white.
For this component, we’ll be using a number between 0.1 and 1 to represent the various visual states
of our button. The exact number isn’t important, since we’re mapping it into a different range. In
this case, lower numbers will render our component smaller and dimmer, and higher numbers will
render it bigger and brighter but we could’ve made it so that higher numbers render the button
smaller.
We want to animate the button whenever the
disabled
prop or the
pressed
state changes. In order
to make the button look and feel good, we’ll animate it to a slightly different size and color for
each combination of disabled and pressed, and we’ll call getValue to get the correct value for each
combination.
The utility function is useful since we don’t need to remember which values to use for each
combination of disabled and pressed. Since getValue is a pure function, we’ll always get
the same result for any given pressed and disabled arguments we pass.
Next, we’ll create a utility method to update this Animated.Value whenever the component’s state
or props change. We want to start a new Animated.timing animation whenever disabled or pressed
change, and we’ll use getValue to determine the desired end value of our Animated.Value for the
animation:
puzzle/components/Button.js
updateValue(nextProps, nextState) {
if (
this.props.disabled !== nextProps.disabled ||
this.state.pressed !== nextState.pressed
) {
Animated.timing(this.value, {
duration: 200,
toValue: getValue(nextState.pressed, nextProps.disabled),
easing: Easing.out(Easing.quad),
}).start();
}
}
We want to run this animation every time the component will update or receive new props:
Animation 408
puzzle/components/Button.js
componentWillUpdate(nextProps, nextState) {
this.updateValue(nextProps, nextState);
}
componentWillReceiveProps(nextProps, nextState) {
this.updateValue(nextProps, nextState);
}
We’ll also create functions for updating the pressed state, which we’ll soon use in the render method:
puzzle/components/Button.js
handlePressIn = () => {
this.setState({ pressed: true });
};
handlePressOut = () => {
this.setState({ pressed: false });
};
Rendering with Animated.Value
Now we’re ready to render our button.
We’ll start by rendering a TouchableWithoutFeedback to handle touches. This is similar to TouchableOpacity
and TouchableHighlight, except that there’s no visual feedback for the user when tapped. That’s
what we want in this case, since we’re defining our own visual feedback with the Animated API.
In addition to the onPress prop that we’ve used with TouchableOpacity, the TouchableWithoutFeedback
components allows us to pass props that handle the pressing down and releasing of the button
separately. The onPressIn prop fires when we touch the button, and the onPressOut prop fires
when we release the button. We can use these to update the visual/animation state of the button
by updating pressed in state. It’s best to use onPressIn and onPressOut for visual feedback, and to
continue using onPress for the behavior of a button (calling the onPress function prop passed into
Button).
We want to render a TouchableWithoutFeedback, passing it an onPress, onPressIn, and onPressOut
prop. Go ahead and delete the existing render method (we don’t need it anymore), and replace it
with the following one:
Animation 409
puzzle/screens/Start.js
// ...
render() {
const {
props: { title, onPress, color, height, borderRadius, fontSize },
} = this;
// ...
return (
<TouchableWithoutFeedback
onPress={onPress}
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
>
{/* ... */}
</TouchableWithoutFeedback>
);
}
// ...
Note that we propagate the onPress prop from our Button component into the TouchableWithoutFeedback.
Within the TouchableWithoutFeedback, we’ll render an Animated.View and an Animated.Text
component:
puzzle/screens/Start.js
// ...
render() {
// ...
return (
<TouchableWithoutFeedback
onPress={onPress}
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
>
<Animated.View style={/* ... */}>
<Animated.Text style={/* ... */}>
{title}
Animation 410
</Animated.Text>
</Animated.View>
</TouchableWithoutFeedback>
);
}
// ...
For both the Animated.View and Animated.Text, we want to use this.value to modify their color
we can do this with this.value.interpolate. As mentioned previously, the interpolate method
lets us interpolate an Animated.Value into a different range. In this case, we’ll interpolate a number
between 0 and 1 into a color between black and the color prop, where 0 represents black and 1
represents fully colored. In render(), let’s make a new variable above the return statement to hold
this color:
puzzle/components/Button.js
const animatedColor = this.value.interpolate({
inputRange: [0, 1],
outputRange: ['black', color],
});
We can use the output of this.value.interpolate as a style attribute for any attribute that accepts a
color. Similarly, we’ll interpolate this.value into a suitable range to update the scale of the button:
puzzle/components/Button.js
const animatedScale = this.value.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 1],
});
In this case, 0 will map to 0.8 and 1 will map to 1. These numbers were chosen by testing the
animation and trying a few different values to see what looked best.
Next we’ll create a style object for our Animated.View:
Animation 411
puzzle/components/Button.js
const containerStyle = {
borderColor: animatedColor,
borderRadius,
height,
transform: [{ scale: animatedScale }],
};
Transform
By using the transform attribute of a component’s style, we can apply a transformation matrix to
the component before rendering it. This allows us to scale, rotate, translate, or skew the component.
Transformations applied this way don’t affect the layout of other components. Even if we make a
component bigger by setting a { scale: 2 } transformation, the sibling and parent components
will remain in the exact same position. If components overlap after a transformation is applied, the
component rendered last will render on top (unless zIndex is used for re-ordering).
Because transform doesn’t affect the layout of our components, we’re able to animate it with
useNativeDriver for improved performance. For this reason, we generally use transform whenever
we can for animations. Rather than animating top or left, we can translate. Rather than animating
width and height, consider a scale transformation. Some of the time we’ll still need to animate
layout properties, but it’s good to consider whether a transform can accomplish the same thing.
If you’re coming from CSS, you might be familiar with the transform attribute already. The idea
is the same, although the API is fairly different. The API in React Native might seem unusual: the
transform attribute takes an array of objects, each with a single key. This object-based description
of transformations, e.g. { scale: 2 }, lets React Native expose a type-safe API when used with
the Flow language. By contrast, the API in CSS is string-based, e.g. scale(2), which can’t be fully
type-checked in Flow. We can’t simplify the API by combining the array of objects into a single
object, since it’s possible to apply several of the same kind of transformation, e.g. [{ scale: 2 },
{ rotateX: '45deg' }, { scale: 1.2 }].
To see all the transformations you can apply to components, check out the React Native documen-
tation
a
.
a
https://facebook.github.io/react-native/docs/transforms.html
And a style object for our Animated.Text:
Animation 412
puzzle/components/Button.js
const titleStyle = {
color: animatedColor,
fontSize,
};
Update the return value as follows:
puzzle/components/Button.js
return (
<TouchableWithoutFeedback
onPress={onPress}
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
>
<Animated.View style={[styles.container, containerStyle]}>
<Animated.Text style={[styles.title, titleStyle]}>
{title}
</Animated.Text>
</Animated.View>
</TouchableWithoutFeedback>
);
Remember to use wrapped components like Animated.View when working with animations!
It’s very easy to forget. If you pass an Animated.Value in the style of a normal View, you’ll
get seemingly-unrelated error messages that are hard to decipher.
Try it out
Save Button.js. Once the app reloads, you should see the button component we just wrote on the
Start screen. We use it both for choosing the size of the puzzle and for the “Start Game button:
Animation 413
If you try tapping the button, you should see the border color, text color, and scale animate when
you press and when you release your finger.
Starting the game
The last thing we need to do on the start screen is enable the transition to the game screen. We’ll
transition when the user taps the “Start Game button at the bottom of the screen.
Open up Start.js again. We’re going to add a function handlePressStart that sets the transitionState
to WillTransitionOut, and configures a LayoutAnimation using the configureTransition utility
function we wrote earlier. After the animation completes we’ll call the onStartGame prop, which
tells the App to unmount this screen and render the game screen instead. Add the following function
to the Start component:
Animation 414
puzzle/screens/Start.js
handlePressStart = async () => {
const { onStartGame } = this.props;
await configureTransition(() => {
this.setState({ transitionState: State.WillTransitionOut });
});
onStartGame();
};
Then we’ll update the render method with two new things.
We’ll pass the handlePressStart function to our Button component’s onPress prop.
If we’re in the WillTransitionOut state, we don’t want to render anything. This will cause
the components on the screen to fade out, due to the LayoutAnimation we configured.
In order to achieve this, we’ll only render our components when transitionState !==
State.WillTransitionOut.
Update the render method to the following:
puzzle/screens/Start.js
render() {
const { size, onChangeSize } = this.props;
const { transitionState } = this.state;
const toggleStyle = { opacity: this.toggleOpacity };
const buttonStyle = { opacity: this.buttonOpacity };
return (
transitionState !== State.WillTransitionOut && (
<View style={styles.container}>
<View style={styles.logo}>
<Logo />
</View>
{transitionState !== State.Launching && (
<Animated.View style={toggleStyle}>
<Toggle
options={BOARD_SIZES}
value={size}
onChange={onChangeSize}
Animation 415
/>
</Animated.View>
)}
{transitionState !== State.Launching && (
<Animated.View style={buttonStyle}>
<Button title={'Start Game'} onPress={this.handlePressStart} />
</Animated.View>
)}
</View>
)
);
}
Try it out
Save Start.js. Once the app reloads, tap the “Start Game” button. You should see the components
on this screen fade out.
Wrapping up the Start screen
We’ve successfully created the start screen for our puzzle game. To do this we used two types of
animations:
LayoutAnimation - These animations automatically transition our UI between two states. This
is especially useful when animating many components and style attributes at once, or when
we don’t know the exact pixel values to use in the animation (e.g. with flex-based layouts). We
can only animate layout attributes, such as width or flex. If we want to animate non-layout
attributes like color, we’ll need to use Animated.
Animated - These animations offer us more control than LayoutAnimation, but also have a
more complex API. We have to specify the exact starting and ending value for each animation,
and start each animation at the appropriate time. We use these animations when animating
multiple components independently, or when animating non-layout attributes like color. We
can use useNativeDriver to improve the performance of our non-layout animations.
Next, we’ll use these same animation techniques to make the Game screen.
Building the Game screen
The Game screen shows the current puzzle, along with a preview of the completed puzzle in the top
left and a timer and moves counter in the top right:
Animation 416
Game lifecycle
We’re going to combine a lot of different animations to show and hide the game screen. Since these
are all asynchronous and will span multiple components, the code can easily get complicated without
careful planning. It’s important that we have a clear understanding of how the animations should
work before we attempt to code them.
Let’s think about the “lifecycle” of the Game screen. There are two phases of the lifecycle: the
“transition in” phase where the screen transitions into view, and the “transition out” phase where it
transitions out of view.
The Game screen renders the Board component as a child. The Board handles a lot of the animations.
In the “transition in” phase, we must first display the Game before we display the Board. In the
“transition out” phase, we must hide the Board before we hide the Game we do this to give the
board time to animate before unmounting it.
The “transition in” phase
Let’s consider the following diagram of the “transition in” phase:
Animation 417
Here’s what needs to happen at each step:
1. After the user presses the start button on the Start screen, the Start screen fades out, and the App
renders the Game screen. The App passes an image and a puzzle state as props to the Game.
The possible states of the Game screen are:
puzzle/screens/Game.js
const State = {
LoadingImage: 'LoadingImage',
WillTransitionIn: 'WillTransitionIn',
RequestTransitionOut: 'RequestTransitionOut',
WillTransitionOut: 'WillTransitionOut',
};
The Game begins in either the LoadingImage or WillTransitionIn state.
2. Since the image may not have fully downloaded yet, it may be null to begin with. If that’s the
case, the Game begins in the LoadingImage state and renders an ActivityIndicator until the image
loads:
Animation 418
3. Otherwise, the
Game
begins in the
WillTransitionIn
state. In this state, the
Game
will render the
top and bottom of the screen, along with an empty game board in the middle:
Animation 419
4. After the
WillTransitionIn
state, it’s the
Board
component’s turn to animate things. The
Board
component handles animating the puzzle pieces into view:
Animation 420
The possible states of the
Board
component are:
puzzle/components/Board.js
const State = {
WillTransitionIn: 'WillTransitionIn',
DidTransitionIn: 'DidTransitionIn',
DidTransitionOut: 'DidTransitionOut',
};
The Board begins in the WillTransitionIn state, and starts animating each puzzle piece from
beneath the screen into the center of the screen. Once these animations finish, it will enter the
DidTransitionIn state and call its onTransitionIn prop (passed in from Game).
5. The Game listens the Board to call onTransitionIn. Once it’s called, the Game will start the timer
in the top right. Now the game has begun!
The “transition out” phase
Now let’s consider how the Game and Board transition out of view:
Animation 421
1. Once the user completes the puzzle or presses the quit button, the game enters the RequestTransitionOut
state. In this state, the Game tells the Board to do any cleanup it needs by passing the prop
teardown={true}.
2. Upon receiving the teardown prop, the Board transitions animates each puzzle piece out of
view. Once this cleanup is done, the Board will transition to the DidTransitionOut state and
call onTransitionOut. We do this to give the Board a chance to animate before we unmount the
component:
3. When the onTransitionOut prop is called, the Game transitions to its final state,WillTransitionOut.
Animation 422
In the WillTransitionOut state, the Game will fade out the entire UI in preparation for displaying
the Start screen again.
Transitioning in and out
Now that we have a plan, we can start our implementation! Open screens/Game.js. Once again,
some of the skeleton of the screen has already been written for you.
You can ignore the handlePressSquare function for now; we’ll come back to that later.
Let’s begin by writing the constructor for the Game screen. Here we’ll use the existence of the image
prop to determine whether we should begin in the LoadingImage or WillTransitionIn state. We’ll
run our configureTransition utility to enqueue a LayoutAnimation on initial render. This covers
step 1 of the “transition in” phase described previously.
Open up Game.js and add the following constructor:
puzzle/screens/Game.js
constructor(props) {
super(props);
const { image } = props;
this.state = {
transitionState: image ? State.WillTransitionIn : State.LoadingImage,
moves: 0,
elapsed: 0,
previousMove: null,
image: null,
};
configureTransition();
}
If the game begins in the LoadingImage state, then we want to watch for when the image prop changes
so we know when to transition to the WillTransitionIn state. This is step 2 of the “transition in”.
Add the following componentWillReceiveProps to do this:
Animation 423
puzzle/screens/Game.js
componentWillReceiveProps(nextProps) {
const { image } = nextProps;
const { transitionState } = this.state;
if (image && transitionState === State.LoadingImage) {
configureTransition(() => {
this.setState({ transitionState: State.WillTransitionIn });
});
}
}
We can update the render method to handle these first few states. If we’re still loading an image (Game
is in the LoadingImage state) we want to show an ActivityIndicator. If we’ve finished loading the
image, we want to show the stats and image preview at the top of the screen. Let’s update our render
method to the following:
puzzle/screens/Game.js
// ...
render() {
const { puzzle, puzzle: { size }, image } = this.props;
const { transitionState, moves, elapsed, previousMove } = this.state;
return (
<View style={styles.container}>
{transitionState === State.LoadingImage && (
<ActivityIndicator size={'large'} color={'rgba(255,255,255,0.5)'} />
)}
{transitionState !== State.LoadingImage && (
<View style={styles.centered}>
<View style={styles.header}>
<Preview image={image} boardSize={size} />
<Stats moves={moves} time={elapsed} />
</View>
{/* ... */}
</View>
)}
</View>
);
}
Animation 424
// ...
Try it out
Save Game.js and reload the app. If you tap the “Start Game” button, you should see the start screen
fade out and the first few components of the game screen fade in.
Transitioning in and out, continued
Now let’s add the game board. The Board component will handle its own transitions once rendered
from Game. Rendering the Board completes step 3 of the “transition in” phase, and step 4 will
be completed within the Board component. Let’s create the methods we need to pass as props
to the Board component. We’ll start by creating a method for handling when the Board finishes
transitioning.
The timer in the top right of the screen counts how much time has elapsed since the game started.
Once the board fully transitions in, it will call its onTransitionIn prop. That’s when we want
to start the timer. We’ll use setInterval to increment state.elapsed every second. Let’s add a
handleBoardTransitionIn method that we can pass to onTransitionIn to do this:
Animation 425
puzzle/screens/Game.js
handleBoardTransitionIn = () => {
this.intervalId = setInterval(() => {
const { elapsed } = this.state;
this.setState({ elapsed: elapsed + 1 });
}, 1000);
};
This completes the last step of the transition in phase. However, we’ll want to add a few more things
before we update the render method. Let’s consider how the Game screen should transition out.
The game ends either when the puzzle is finished or when the user presses the “Quit” button. In
both of these scenarios, we want to enter the transitionState called RequestTransitionOut. In
this state, we give the Board time to run its transition animation.
Let’s add a requestTransitionOut function to Game that handles updating transitionState. We also
want to stop the timer in the top right once we call this:
puzzle/screens/Game.js
requestTransitionOut = () => {
clearInterval(this.intervalId);
this.setState({ transitionState: State.RequestTransitionOut });
};
Pressing the quit button should also set the transitionState to RequestTransitionOut. We can call
the same requestTransitionOut function for this. When the quit button is pressed, we’ll handle
it with a new function handlePressQuit. We’ll use the Alert API, which we covered in the “Core
APIs” chapter, to ask the user if they are sure they want to quit. Add the following function to Game:
puzzle/screens/Game.js
handlePressQuit = () => {
Alert.alert(
'Quit',
'Do you want to quit and lose progress on this puzzle?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Quit',
style: 'destructive',
onPress: this.requestTransitionOut,
},
Animation 426
],
);
};
That handles step 1 of the “transition out” phase. The Board component will handle step 2 internally.
Once the board transitions out (remember, we had to give it time to transition before unmounting),
it will call its onTransitionOut prop. When this happens, we want to fade out the components
on this game screen. We can do that using our configureTransition utility function. After
that we want to call the onQuit() prop, which will return us to the start screen. Let’s write a
handleBoardTransitionOut function which we’ll pass to onTransitionOut:
puzzle/screens/Game.js
handleBoardTransitionOut = async () => {
const { onQuit } = this.props;
await configureTransition(() => {
this.setState({ transitionState: State.WillTransitionOut });
});
onQuit();
};
That completes the “transition out” phase! Now we can update the render method to render both
the Board and the “Quit” Button.
Before returning anything, we’ll first check if we’re in the WillTransitionOut state if we are,
we don’t want to render anything, since we want our LayoutAnimation to fade the entire screen
out. We also need to remember to pass the teardown prop to the Board when Game is in the
RequestTransitionOut state.
Let’s update the render method to the following:
puzzle/screens/Game.js
render() {
const { puzzle, puzzle: { size }, image } = this.props;
const { transitionState, moves, elapsed, previousMove } = this.state;
return (
transitionState !== State.WillTransitionOut && (
<View style={styles.container}>
{transitionState === State.LoadingImage && (
<ActivityIndicator size={'large'} color={'rgba(255,255,255,0.5)'} />
)}
Animation 427
{transitionState !== State.LoadingImage && (
<View style={styles.centered}>
<View style={styles.header}>
<Preview image={image} boardSize={size} />
<Stats moves={moves} time={elapsed} />
</View>
<Board
puzzle={puzzle}
image={image}
previousMove={previousMove}
teardown={transitionState === State.RequestTransitionOut}
onMoveSquare={this.handlePressSquare}
onTransitionOut={this.handleBoardTransitionOut}
onTransitionIn={this.handleBoardTransitionIn}
/>
<Button title={'Quit'} onPress={this.handlePressQuit} />
</View>
)}
</View>
)
);
}
Try it out
Save Game.js and reload the app. If you tap the “Start Game” button, you should see the start screen
fade out and the game screen fade in:
Animation 428
The board won’t render yet, but we’ll add that in the next chapter. If you tap the “Quit” button, you
should see a dialog with options to “Cancel” or “Quit”:
Animation 429
These won’t do anything yet though, since the
Board
will never call its
onTransitionOut
prop.
That completes the Game screen, for now. In the next chapter, we’ll build the Board.
Summary
In this chapter, we used the two main animation APIs included in React Native: Animated and
LayoutAnimation. We used LayoutAnimation to move components around the screen, and to fade
components into and out of view. We used Animated to animate non-layout styles like colors and
when we wanted more control over individual animations.
In the next chapter (“Gestures”), we’ll finish our puzzle game. We’ll build the Board component
and allow the user to rearrange puzzle pieces by dragging. Gestures rely heavily on Animated, so
we’ll see a few more ways we can use the Animated API, while ensuring smooth, high-performance
animations.
Gestures
Gestures are fundamental to building mobile apps. Well-designed gestures can make mobile apps
feel intuitive and easy to use. Just like with animations, we often use gestures to imitate movement
in the real world or to build interactivity similar to familiar physical objects. For this reason, most
gestures are accompanied by animations physical objects rarely teleport from one place to another,
so neither should components in our UI. We can leverage the Animated API from the previous chapter
to build gestures that feel natural.
Simple gestures are supported out-of-the-box by React Native components. When we want to add
a tap gesture, we can use TouchableOpacity or TouchableHighlight. For more advanced gestures,
however, we’ll need to create our own components using lower-level APIs. In this chapter we’ll
explore gestures by adding an interactive game board to the puzzle app we started in the previous
chapter.
Picking up where we left off
We successfully built the start screen and we started the game screen for our puzzle app. Next, we’re
going to add the interactive board.
This is a code checkpoint. If you haven’t been coding along with us but would like to start
now, we’ve included a snapshot of our current progress in the sample code for this book.
First, copy the puzzle/2 directory to somewhere else on your computer. Then in the terminal,
you can navigate to the directory you copied and install the dependencies by running yarn.
For example, if you unzipped the book’s sample code to
/Downloads/fsrn/
, on a macOS
or Linux computer you would run:
$ cp -r ~/Downloads/fsrn/puzzle/2 ~/Downloads/puzzle
$ cd ~/Downloads/puzzle
$ yarn
Building the board
Our Board component is responsible for animating the puzzle pieces into view when the components
mount, handling the drag gesture as the user moves the pieces, and animating the pieces out of view
when the game ends.
Gestures 431
The Board component has already been started for you. Let’s take a look at what’s already there.
Open Board.js.
Transition states
Just like with the Start and Game screens we built in the previous chapter, we’ll use a transitionState
prop to control the various transitions in the Board. These are the states of the board:
puzzle/components/Board.js
const State = {
WillTransitionIn: 'WillTransitionIn',
DidTransitionIn: 'DidTransitionIn',
DidTransitionOut: 'DidTransitionOut',
};
The Board begins in the WillTransitionIn state, and starts animating each puzzle piece from
beneath the screen into the center of the screen:
Once these animations finish, it will enter the DidTransitionIn state and call its onTransitionIn
prop (passed in from Game). At this point, the puzzle pieces become interactive:
Gestures 432
The
Game
will tell the board when the game is finished and it’s time to cleanup by passing the
teardown prop. Upon receiving the teardown prop, the Board transition animates each puzzle piece
out of view:
Gestures 433
Once this cleanup is done, the
Board
will transition to the
DidTransitionOut
state and call
onTransitionOut.
Board props
Next lets look at the propTypes for this component:
puzzle/2/1/components/Board.js
static propTypes = {
puzzle: PuzzlePropType.isRequired,
teardown: PropTypes.bool.isRequired,
image: Image.propTypes.source,
previousMove: PropTypes.number,
onMoveSquare: PropTypes.func.isRequired,
onTransitionIn: PropTypes.func.isRequired,
onTransitionOut: PropTypes.func.isRequired,
};
The board is passed the current state of the puzzle, the image, and the previousMove. From these,
the board can determine how to render the puzzle. The board will never modify the state of the
Gestures 434
puzzle instead, the board will call onMoveSquare to inform the Game component that a piece has
been moved.
We use a PropTypes.shape to validate the fields within the puzzle object:
puzzle/validators/PuzzlePropType.js
import PropTypes from 'prop-types';
export default PropTypes.shape({
size: PropTypes.number.isRequired,
empty: PropTypes.number.isRequired,
board: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired,
});
The puzzle object contains the size of the board, the arrangement of pieces on the board, and an
indicator recording which piece is the empty piece. Each piece is represented by a number. In its
finished state, the piece numbers will be properly sorted from small to large within board. In other
words, the number represents the correct” or “final” position of the piece in the completed puzzle.
The empty value refers to the number of a piece (not to an index in the board array).
If we overlay these numbers on top of the puzzle board, we can see how each number corresponds
with a piece:
Gestures 435
For this example, the initial state of a puzzle object is:
{
size: 3,
empty: 8,
board: [7, 3, 1, 6, 8, 2, 0, 4, 5]
}
When completed, the puzzle object will be:
{
size: 3,
empty: 8,
board: [0, 1, 2, 3, 4, 5, 6, 7, 8]
}
We’ll use the piece numbers directly when rendering the board. We’ll use utility functions for
determining anything else from the puzzle object.
Gestures 436
If you recall from the previous chapter, each puzzle uses a random image fetched from a remote API.
When we render the puzzle, we’ll need to “split up” the image into a grid of puzzle pieces.
We won’t actually modify the raw image data instead we’ll render the same image multiple times,
once for each piece, and offset the image’s position. We can use a style with overflow: hidden to
hide the excess parts of the image we don’t want to show. We’ll use the piece’s number to calculate
the position of the image for that piece.
You’ll notice at the top of the file we import two utility functions:
import { availableMove, getIndex } from '../utils/puzzle';
We’ll use availableMove to determine which directions the user may drag any given piece. And
we’ll use getIndex to determine the current position of any given piece.
The other props, teardown, onTransitionIn, and onTransitionOut, are all used to communicate state
changes between the Game and Board components.
Initializing the board
We’ll start by writing a simplified version of the game board where the pieces don’t move. After we
have the pieces showing up in the correct positions, we’ll add the animation and gestures.
Each piece on the board will use an Animated.Value to represent its top, left, and scale. This
gives us fine-grained control over the animations of each piece. We can use a helper function,
calculateItemPosition, already imported at the top of the file to determine the correct starting
top and left position of each piece.
Our constructor needs to do 2 things:
Initialize the transitionState to WillTransitionIn
Create an Animated.Value for the top, left, and scale of each piece
Add the following constructor to components/Board.js:
Gestures 437
puzzle/2/1/components/Board.js
constructor(props) {
super(props);
const { puzzle: { size, board } } = props;
this.state = { transitionState: State.WillTransitionIn };
this.animatedValues = [];
board.forEach((square, index) => {
const { top, left } = calculateItemPosition(size, index);
this.animatedValues[square] = {
scale: new Animated.Value(1),
top: new Animated.Value(top),
left: new Animated.Value(left),
};
});
}
Recall from the previous chapter that an Animated.Value wraps a number. We need to
instantiate a separate Animated.Value for the top, left, and scale of each puzzle piece
on the board, since we want to animate all of these values independently.
We’ll render each puzzle piece with an absolute position, so that it renders at the top-left of the
board. Then we’ll use the top and left animated values to position the piece relative to the top-left
of the board.
Now that we have our constructor, we’ll also need a componentDidMount method where we:
Start the initial animation (where the puzzle pieces fly onto the board)
Set transitionState to DidTransitionIn once the animation completes
Call onTransitionIn to inform the Game that the transition animation has completed and the
game has begun
We’ll handle starting the transition animation later in the chapter, so for now, let’s add a
componentDidMount method that sets the transitionState and calls onTransitionIn:
Gestures 438
puzzle/2/1/components/Board.js
async componentDidMount() {
const { onTransitionIn } = this.props;
this.setState({ transitionState: State.DidTransitionIn });
onTransitionIn();
}
Rendering the board
Next, let’s render each piece on the game board. In order to determine the proper size of the board
and each piece, we’ll use two utility functions that have already been imported at the top of the file:
calculateContainerSize() - This function returns the size to render the board, in pixels. Since
the board is a square, we’ll use this size for both the width and height.
calculateItemSize(size) - This function uses puzzle.size to divide the board into an even
number of rows and columns. We’ll use the returned pixel size for the width and height of
each piece.
We’ll represent the board with a View. We’ll map each puzzle piece in puzzle.board into an
Animated.View that contains an Image.
Add the following render method to components/Board.js:
puzzle/2/1/components/Board.js
render() {
const { puzzle: { board } } = this.props;
const { transitionState } = this.state;
const containerSize = calculateContainerSize();
const containerStyle = { width: containerSize, height: containerSize };
return (
<View style={[styles.container, containerStyle]}>
{transitionState !== State.DidTransitionOut &&
board.map(this.renderSquare)}
</View>
);
}
Gestures 439
Notice that we map each piece in the board through this.renderSquare. Let’s write the renderSquare
method now. This method is called with two arguments:
square - The numeric value of the piece in puzzle.board
index - The index of the square within the puzzle.board array
In other words, the square represents the “correct” position of the puzzle piece (within the original
image), while the index represents the current position of the puzzle piece (within the rearranged
image).
When the board is in the DidTransitionOut state, we don’t render any pieces. This shouldn’t
be necessary, since the pieces should have already animated off-screen. However, there’s a
bug that occurs when combining Animated and useNativeDriver that causes the pieces to
render without their transform styles.
Let’s write renderSquare now. We’ll start by declaring the method and destructuring the props and
state we’ll need:
puzzle/2/1/components/Board.js
renderSquare = (square, index) => {
const { puzzle: { size, empty }, image } = this.props;
const { transitionState } = this.state;
If the square is the empty square of the puzzle (puzzle.empty), then we shouldn’t render it:
puzzle/2/1/components/Board.js
renderSquare = (square, index) => {
const { puzzle: { size, empty }, image } = this.props;
const { transitionState } = this.state;
if (square === empty) return null;
Next, we’ll call calculateItemSize to get the pixel size of the puzzle piece. This value will be the
same for every piece:
Gestures 440
puzzle/2/1/components/Board.js
if (square === empty) return null;
const itemSize = calculateItemSize(size);
We can use the itemSize to create a style, itemStyle, for the Animated.View that we’ll render. The
view should have a width and height equal to itemSize, and use a transform to correctly position
it on the board. This is where the Animated.Value array we set up earlier comes in:
puzzle/2/1/components/Board.js
const itemSize = calculateItemSize(size);
const itemStyle = {
position: 'absolute',
width: itemSize,
height: itemSize,
overflow: 'hidden',
transform: [
{ translateX: this.animatedValues[square].left },
{ translateY: this.animatedValues[square].top },
{ scale: this.animatedValues[square].scale },
],
};
Note that we use a transform with translateX and translateY, instead of left and
top. If you recall from the previous chapter, this allows us to animate these values with
useNativeDriver for improved performance. In this chapter, we use the names top and left
to refer to a piece’s position for simplicity, even though we’re actually setting the translateX
and translateY.
Within the Animated.View that uses this style, we’ll render the image of the puzzle piece. With some
clever math, we can offset the image to display the correct portion for each piece:
Gestures 441
puzzle/2/1/components/Board.js
const imageStyle = {
position: 'absolute',
width: itemSize * size + (itemMargin * size - 1),
height: itemSize * size + (itemMargin * size - 1),
transform: [
{
translateX: -Math.floor(square % size) * (itemSize + itemMargin),
},
{
translateY: -Math.floor(square / size) * (itemSize + itemMargin),
},
],
};
The exact calculations here and elsewhere in the chapter are specific to this game, so we
won’t cover them in much detail. However, animation and gesture code in general tends to
rely on manual calculations, so you may find it useful to try to understand the calculations
and utility functions in this chapter.
Lastly, we can put everything together by rendering an Animated.View and an Image:
puzzle/2/1/components/Board.js
return (
<Animated.View key={square} style={itemStyle}>
<Image style={imageStyle} source={image} />
</Animated.View>
);
};
Try it out!
Save Board.js. After the app reloads, press the start button, and you should see the board fade in!
Gestures 442
Making pieces draggable
Now that we’re rendering our puzzle pieces, we can focus on making them draggable. In order to
do this, we’ll need to learn how to use the Gesture Responder System.
Gesture Responder System
React Native provides the Gesture Responder System for building complex interactions like drag-
ging. The Gesture Responder System gives us fine-grained control over which components should
receive and respond to touch events.
Each time the user touches the screen, moves their finger, or lifts their finger, the operating system
records an independent event called a “touch event. Interpreting one or more of these independent
touch events results in a gesture. A tap gesture may consist of the user touching the screen and
lifting their finger immediately. A drag gesture may consist of a user touching the screen, moving
their finger around the screen, and then lifting their finger.
Touch events can interact in complex ways in mobile apps. Imagine a horizontally draggable slider
within a vertical scrollview how do we determine which finger movements should affect the slider
and which should affect the scrollview? The Gesture Response System gives us a set of callbacks
which help us handle the right touch events from the right component.
Gestures 443
Responder lifecycle
Let’s look at how touch events flow between components in the responder system.
At its core, the responder system determines which view owns the global “interaction lock” at any
given time. When granted the interaction lock, a view is known as the “responder”, since it responds
to touch events. Generally the responder view should show visual feedback, such as highlighting or
moving. While a touch gesture is occuring, the interaction lock may be transferred to an ancestor
view of the responder.
There are function props a view can implement to request the interaction lock:
View.props.onStartShouldSetResponder: (e) => true - If a touch gesture begins on this
view, should this view become the responder?
View.props.onMoveShouldSetResponder: (e) => true - If the user moves their finger over
this view during a touch gesture, should this view become the responder?
If one of these functions returns true, then the view has requested the interaction lock. If no other
view currently owns the interaction lock, then the requesting view automatically becomes the re-
sponder. If a view does own the interaction lock, then the owner’s onResponderTerminationRequest
function prop will be called the owner can decide whether to keep or hand off the interaction lock.
One of the following props will be called for the view requesting the interaction lock:
View.props.onResponderGrant: (e) => {} - The request for the interaction lock was granted!
The requesting view is now the responder. The view may want to respond to receiving the
interaction lock in some way, e.g. by setting some state that renders a highlight.
View.props.onResponderReject: (e) => {} - The request for the interaction lock was rejected.
The view that owned the interaction lock refused to give it up.
The responder view’s props will be called as the touch gesture continues:
View.props.onResponderMove: (e) => {} - This is called whenever the user moves their finger.
View.props.onResponderRelease: (e) => {} - This is called when the user lifts their finger.
View.props.onResponderTerminationRequest: (e) => true - Another view has requested the
interaction lock. Return false to retain the lock, or true to hand it off.
View.props.onResponderTerminate: (e) => {} - The interaction lock has been taken away.
This is generally due to returning true from onResponderTerminationRequest, but there are
cases where the operating system will take the interaction lock without asking (without
onResponderTerminationRequest being called first). For example, onResponderTerminate will
be called upon receiving a phone call onResponderTerminationRequest will not be called,
since returning false would not prevent the operating system from taking over control of the
screen.
When a touch starts, the onStartShouldSetResponder prop will be called for the inner-most view
containing the touch. If the view returns false (or doesn’t implement the prop), then the parent’s
onStartShouldSetResponder prop will be called. This process continues up the view hierarchy.
Gestures 444
Capture phase
Sometimes a parent view will need to request the interaction lock before any of its children have
a change. In this case, we don’t want the bottom-up calling order of onStartShouldSetResponder.
There are two other props we can use that are called top-down, i.e. first the parent has a chance to
become responder, then the child.
View.props.onStartShouldSetResponderCapture: (e) => true
View.props.onMoveShouldSetResponderCapture: (e) => true
These props are analogous to onStartShouldSetResponder and onMoveShouldSetResponder. These
capture props are called first starting from the parent, and then down the view hierarchy. If no
view returns true, then onStartShouldSetResponder and onMoveShouldSetResponder will be called
starting from the inner-most view.
The portion of the responder lifecycle where touches are “captured” from top to bottom is called the
capture phase. The portion of the lifecycle where touches are capture from bottom to top is called
the bubble phase.
If you’re coming from web development, you’ll probably be familiar with these terms this
part of the responder system is modeled after DOM events!
Diagram
The following diagram illustrates the responder lifecycle for new touches:
Gestures 445
The same flow happens every time a touch moves, except that onMoveShouldSetResponder and
onMoveShouldSetResponderCapture are called instead of onStartShouldSetResponder and onStartShouldSetResponderCapture.
Gestures 446
Touch event object
All of the responder function props are called with an event object, often abbreviated as evt or e, e.g.
the argument in onStartShouldSetResponder = (e) => {}. The event object contains the property
nativeEvent which is an object containing:
locationX - The X position of the touch, relative to the responder view
locationY - The Y position of the touch, relative to the responder view
pageX - The X position of the touch, relative to the root view
pageY - The Y position of the touch, relative to the root view
timestamp - The time when the touch occurred
identifier - The id of the touch
target - The id of the view receiving the touch event
touches - Array of all current touches on the screen
changedTouches - Array of all touch events that have changed since the last event
You may need to access these properties to do touch-related calculations. However, React Native
provides a higher-level convenience wrapper on top of the responder system that you’ll likely use
instead.
PanResponder
The touch event object contains raw values, such as the time and location of touches. Many
interactions, however, will require the distance and velocity of the touches over time. React Native
provides a higher-level API called PanResponder.
The PanResponder intercepts each responder function so that it can maintain a gestureState
object containing the distance, velocity, and a few other computed properties. We can create a
PanResponder with PanResponder.create(config), where the config object contains any of the
following functions:
onStartShouldSetPanResponder: (e, gestureState) => {}
onStartShouldSetPanResponderCapture: (e, gestureState) => {}
onMoveShouldSetPanResponder: (e, gestureState) => {}
onMoveShouldSetPanResponderCapture: (e, gestureState) => {}
onPanResponderReject: (e, gestureState) => {}
onPanResponderGrant: (e, gestureState) => {}
onPanResponderStart: (e, gestureState) => {}
onPanResponderEnd: (e, gestureState) => {}
onPanResponderRelease: (e, gestureState) => {}
onPanResponderMove: (e, gestureState) => {}
Gestures 447
onPanResponderTerminate: (e, gestureState) => {}
onPanResponderTerminationRequest: (e, gestureState) => {}
onShouldBlockNativeResponder: (e, gestureState) => {}
Each of these wraps a responder function (with roughly the same name) and then calls it with
the gestureState object in addition to the original event object. For example, onPanResponderMove
wraps onResponderMove.
The only exception is onShouldBlockNativeResponder, which is a totally different function
from the underlying responder functions. This function is for blocking native components
from becoming the responder, and only works on Android. We won’t be using it.
The gestureState object contains the following properties:
stateID - The id of the gestureState persisted as long as there’s at least one touch on screen
moveX - The latest screen coordinates of the most recently moved touch
moveY - The latest screen coordinates of the most recently moved touch
x0 - The screen coordinates at the time the responder was granted
y0 - The screen coordinates at the time the responder was granted
dx - Accumulated distance of the gesture since the touch started
dy - Accumulated distance of the gesture since the touch started
vx - Current velocity of the gesture
vy - Current velocity of the gesture
numberActiveTouches - Number of touches currently on screen
Now that we’ve covered the fundamentals, let’s put them to use!
Draggable component
For our puzzle game, we want to build a dragging gesture. In order to do this, we’ll need to:
Handle when the user touches a specific puzzle piece component
Continously monitor the x, y offset as they move their finger
Record the final x, y offset when they lift their finger
As the user moves their finger, we can monitor the x, y offset to update the puzzle piece component’s
transform. Thus the puzzle piece will follow the user’s finger as it moves across the screen this is
how we simulate “dragging” in mobile apps. When the user lifts their finger, we will use the final
x, y offset to determine how to update the puzzle object accordingly.
Gestures 448
Creating a PanResponder
Let’s use a PanResponder to make a Draggable component. We’ll use our Draggable component to
handle touches and to monitor the x, y offset of the drag.
We’ll make this component fairly generic, both to better separate the dragging code from the
rendering code, and to make the component easy to reuse. The component’s sole purpose will be
to handle the logic around touch events. It won’t render anything to the UI itself. The Draggable
component will pass a PanResponder and x, y offset to its children its children will be responsible
for using this information to render the UI. The Draggable component will expose callback props
that notify the parent as touch events occur. We’ll use these events to update the puzzle object.
Open up components/Draggable.js. The skeleton of the component has already been written for
you. Take a look at the propTypes:
puzzle/2/components/Draggable.js
static propTypes = {
children: PropTypes.func.isRequired,
onTouchStart: PropTypes.func,
onTouchMove: PropTypes.func,
onTouchEnd: PropTypes.func,
enabled: PropTypes.bool,
};
This component will render arbitrary children by calling the children function prop. When the user
interacts with the component, it will call the onTouchStart, onTouchMove, and onTouchEnd props at
the appropriate times. We’ve also included an enabled prop to prevent dragging we’ll use this
when the board is mounting and unmounting so that the user can’t interfere with the animation.
We chose to name our props onTouchStart, onTouchMove, and onTouchEnd after the touch
event names on the web, in order to make our Draggable component feel more familiar for
those with a web background. However, React Native doesn’t interpret these names in any
special way, and we could’ve named the props anything we wanted.
Using a function for children might look familiar. That’s because we used this same pattern
in the second part of the “Core APIs” chapter!
Let’s begin by writing the constructor. In the constructor, we’ll initialize this.state to keep track
of whether this component is actively being dragged. We’ll also create a new pan responder with
PanResponder.create.
When we write the render method, we’ll pass the value of this.state.dragging to the Draggable
component’s children. This allows them to render differently depending on whether a drag is
Gestures 449
occuring or not. In our case, we will use this to adjust the zIndex of puzzle pieces so that the piece
currently being dragged renders on top of the rest.
Add the following constructor to Draggable.js:
puzzle/components/Draggable.js
constructor(props) {
super(props);
this.state = {
dragging: false,
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: this.handleStartShouldSetPanResponder,
onPanResponderGrant: this.handlePanResponderGrant,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderEnd,
onPanResponderTerminate: this.handlePanResponderEnd,
});
}
We’ll implement each of the pan responder functions as instance properties of our component.
PanResponder functions
Let’s start by implementing this.handleStartShouldSetPanResponder, which we assigned to
onStartShouldSetPanResponder when creating our pan responder. We want this component to
become the responder when the enabled prop is true. Add the following function to the Draggable
component:
puzzle/components/Draggable.js
handleStartShouldSetPanResponder = () => {
const { enabled } = this.props;
return enabled;
};
Next, we’ll handle when this component becomes the responder in handlePanResponderGrant.
When this component becomes the responder, we want to set state.dragging to true. Add
handlePanResponderGrant now:
Gestures 450
puzzle/components/Draggable.js
handlePanResponderGrant = () => {
const { onTouchStart } = this.props;
this.setState({ dragging: true });
onTouchStart();
};
We’ll pass a the onTouchStart() prop later to allow the parent to animate the scale of the puzzle
piece when a drag begins.
Any time the user moves their finger, we’ll call the onTouchMove prop with the offset from the initial
touch position. This lets us animate the transform of the puzzle piece as it’s dragged from within
Board. Let’s do this by adding the following handlePanResponderMove:
puzzle/components/Draggable.js
handlePanResponderMove = (e, gestureState) => {
const { onTouchMove } = this.props;
// Keep track of how far we've moved in total (dx and dy)
const offset = {
top: gestureState.dy,
left: gestureState.dx,
};
onTouchMove(offset);
};
Lastly, when the user lifts their finger, or if the operating system cancels the gesture (e.g. a phone
call comes in), we want to reset our state and call onTouchMove and onTouchEnd with the final touch
position. We can handle both onPanResponderRelease and onPanResponderTerminate in the same
way. Add the following handlePanResponderEnd:
Gestures 451
puzzle/components/Draggable.js
handlePanResponderEnd = (e, gestureState) => {
const { onTouchMove, onTouchEnd } = this.props;
const offset = {
top: gestureState.dy,
left: gestureState.dx,
};
this.setState({
dragging: false,
});
onTouchMove(offset);
onTouchEnd(offset);
};
If the user receives a phone call, the drag gesture will end as if the user had lifted their
finger. This is acceptable for our game, but in some scenarios it may be desirable to handle
the onPanResponderTerminate case differently from an intentional touch event.
Rendering draggable pieces
When we render the Draggable component, we’ll call the children prop function. We’ll pass this
function the dragging and offset values from state so that we can update the positions of the
children as we render. We’ll also pass this.panResponder.panHandlers these are the responder
functions wrapped by the pan handler to include gestureState.
Add the following render method:
puzzle/components/Draggable.js
render() {
const { children } = this.props;
const { dragging } = this.state;
// Update children with the state of the drag
return children({
handlers: this.panResponder.panHandlers,
dragging,
});
}
Gestures 452
Save Draggable.js. Now open Board.js again so we can render the Draggable component from our
renderSquare method.
There are three changes we’ll be making to renderSquare:
We’ll return a Draggable component. We’ll move what we’re currently rendering into the
children function prop of Draggable. The children function is passed handlers, dragging, and
offset from the Draggable component, and should return the children component to render.
For the other props of Draggable, we need to set the enabled prop to true once the board’s
pieces have transitioned in so we’ll check if the board is in the DidTransitionIn state. We’ll
also set onTouchStart, onTouchMove, and onTouchEnd props, which we’ll write shortly.
We’ll update itemStyle to use the draggable argument of the children function. When
draggable is true, we want to set the zIndex of the piece to 1. This will ensure the piece renders
above the adjacent pieces when its being dragged (we’ll increase its scale during the drag, so
it will overlap the edges of the adjacent pieces).
We’ll update the Animated.View by spreading the handlers argument of the children function
into the Animated.View props. This is how we apply the PanResponder to a view.
Let’s makes these updates to renderSquare now:
puzzle/components/Board.js
const itemSize = calculateItemSize(size);
return (
<Draggable
key={square}
enabled={transitionState === State.DidTransitionIn}
onTouchStart={() => this.handleTouchStart(square)}
onTouchMove={offset => this.handleTouchMove(square, index, offset)}
onTouchEnd={offset => this.handleTouchEnd(square, index, offset)}
>
{({ handlers, dragging }) => {
const itemStyle = {
position: 'absolute',
width: itemSize,
height: itemSize,
overflow: 'hidden',
transform: [
{ translateX: this.animatedValues[square].left },
{ translateY: this.animatedValues[square].top },
{ scale: this.animatedValues[square].scale },
],
zIndex: dragging ? 1 : 0,
Gestures 453
};
const imageStyle = {
position: 'absolute',
width: itemSize * size + (itemMargin * size - 1),
height: itemSize * size + (itemMargin * size - 1),
transform: [
{
translateX:
-Math.floor(square % size) * (itemSize + itemMargin),
},
{
translateY:
-Math.floor(square / size) * (itemSize + itemMargin),
},
],
};
return (
<Animated.View {...handlers} style={itemStyle}>
<Image style={imageStyle} source={image} />
</Animated.View>
);
}}
</Draggable>
);
};
The above code snippet is pretty long, but we only actually made a few changes. One of the most
important lines is the one where we spread the handlers into the Animated.View:
puzzle/components/Board.js
<Animated.View {...handlers} style={itemStyle}>
Without this, our Draggable component won’t respond to touches!
For each of the Draggable props onTouchStart, onTouchMove, and onTouchEnd, we’ll call the Board
methods handleTouchStart, handleTouchMove, and handleTouchEnd, respectively. These handler
methods don’t exist yet, so let’s write them now.
Handling touch events
When a touch begins on a piece, we want to scale the piece so that it gives the illusion of lifting it.
We can do that by animating this.animatedValues[square].scale. We’ll use the Animated.spring
Gestures 454
function to scale the piece to 1.1 times its original size. Add the following handleTouchStart method
to the Board component:
puzzle/components/Board.js
handleTouchStart(square) {
Animated.spring(this.animatedValues[square].scale, {
toValue: 1.1,
friction: 20,
tension: 200,
useNativeDriver: true,
}).start();
}
As always, we have to call start() after setting up the animation.
Every time a touch moves, we want to update this.animatedValues[square].left and this.animatedValues[square].top
for the piece. We’ll need to restrict the piece’s movement according to which square is currently
empty. We can determine this by calling the availableMove utility function. This function returns a
string, 'up', 'down', 'left','right', or 'none', depending on the state of the game board. Based on
this, we can restrict the values of top and left to only allow the valid moves according to our game’s
rules. The exact math we use is specific to this game, so it’s not important to follow it completely.
Add the following handleTouchMove to Board:
puzzle/components/Board.js
handleTouchMove(square, index, { top, left }) {
const { puzzle, puzzle: { size } } = this.props;
const itemSize = calculateItemSize(size);
const move = availableMove(puzzle, square);
const { top: initialTop, left: initialLeft } = calculateItemPosition(
size,
index,
);
const distance = itemSize + itemMargin;
const clampedTop = clamp(
top,
move === 'up' ? -distance : 0,
Gestures 455
move === 'down' ? distance : 0,
);
const clampedLeft = clamp(
left,
move === 'left' ? -distance : 0,
move === 'right' ? distance : 0,
);
this.animatedValues[square].left.setValue(initialLeft + clampedLeft);
this.animatedValues[square].top.setValue(initialTop + clampedTop);
}
We have two options for updating the top and left Animated.Value. We could either
animate them (e.g. using Animated.spring), or update them immediately with setValue.
After testing both ways, we found using setValue provides a slightly better experience on
slower devices in this specific case.
Before we handle when touches end, let’s first write a utility method, updateSquarePosition, that
animates a piece’s position. When updating a piece’s position, we’ll update both the top and left
values at the same time for simplicity (even though a piece can only be moved along one axis at a
time).
Begin the updateSquarePosition method with the following:
puzzle/components/Board.js
updateSquarePosition(puzzle, square, index) {
const { size } = puzzle;
const { top, left } = calculateItemPosition(size, index);
const animations = [
Animated.spring(this.animatedValues[square].top, {
toValue: top,
friction: 20,
tension: 200,
useNativeDriver: true,
}),
Animated.spring(this.animatedValues[square].left, {
toValue: left,
friction: 20,
tension: 200,
Gestures 456
useNativeDriver: true,
}),
];
Since we’re going to animate multiple values at once, we’ve created an animations array containing
all of our animation objects. Notice that we didn’t call start() on these animations. We need to know
when both animations have completed, which is hard to determine if we start them independently.
Fortunately, the React Native provides the Animated.parallel for this case.
Animated.parallel takes an array of animations, and returns an object with a start(callback)
method, just like with other animations. The callback is called when every animation in the array
is completed. To make our updateSquarePosition slightly more convenient to use (with async/await
syntax), we’ll return a Promise that resolves when the callback is called.
Update the updateSquarePosition method to call Animated.parallel and return a Promise now:
puzzle/components/Board.js
updateSquarePosition(puzzle, square, index) {
const { size } = puzzle;
const { top, left } = calculateItemPosition(size, index);
const animations = [
Animated.spring(this.animatedValues[square].top, {
toValue: top,
friction: 20,
tension: 200,
useNativeDriver: true,
}),
Animated.spring(this.animatedValues[square].left, {
toValue: left,
friction: 20,
tension: 200,
useNativeDriver: true,
}),
];
return new Promise(resolve => Animated.parallel(animations).start(resolve));
}
Now that we’ve finished updateSquarePosition, we can write the handleTouchEnd method to finish
handling touch events.
When a touch ends, we need to do a few things:
Gestures 457
We need to scale the piece back to its original size (a scale value of 1) using Animated.spring.
We need to detect whether the user dragged the piece far enough to move it or not. If the piece
was moved more than halfway into the empty square, then we’ll consider this a move, and
we’ll inform the Game of the move by calling the onMoveSquare prop. If the piece was moved
less than halfway into the empty square, then we won’t consider this a move, and we’ll instead
animate the piece back to its original position.
Begin the handleTouchEnd with the following to reset the piece’s scale:
puzzle/components/Board.js
handleTouchEnd(square, index, { top, left }) {
const { puzzle, puzzle: { size }, onMoveSquare } = this.props;
const itemSize = calculateItemSize(size);
const move = availableMove(puzzle, square);
Animated.spring(this.animatedValues[square].scale, {
toValue: 1,
friction: 20,
tension: 200,
useNativeDriver: true,
}).start();
Based on the direction the piece was moved, we can determine if it was moved more than halfway
to its destination. We’ll finish the handleTouchEnd method by either calling onMoveSquare to inform
the Game of a successful move, or by calling updateSquarePosition to reset the piece’s position:
puzzle/components/Board.js
handleTouchEnd(square, index, { top, left }) {
const { puzzle, puzzle: { size }, onMoveSquare } = this.props;
const itemSize = calculateItemSize(size);
const move = availableMove(puzzle, square);
Animated.spring(this.animatedValues[square].scale, {
toValue: 1,
friction: 20,
tension: 200,
useNativeDriver: true,
}).start();
if (
Gestures 458
(move === 'up' && top < -itemSize / 2) ||
(move === 'down' && top > itemSize / 2) ||
(move === 'left' && left < -itemSize / 2) ||
(move === 'right' && left > itemSize / 2)
) {
onMoveSquare(square);
} else {
this.updateSquarePosition(puzzle, square, index);
}
}
After a successful move, the Game will update the puzzle object and pass it back into the Board
as a prop, along with the previousMove prop. We need to handle updates to these props in
componentWillReceiveProps.
Whenever the puzzle object updates, we’ll call updateSquarePosition to animate the piece that was
last moved into its new position. To do this, add the following componentWillReceiveProps to Board:
puzzle/2/2/components/Board.js
async componentWillReceiveProps(nextProps) {
const { previousMove, onTransitionOut, puzzle, teardown } = nextProps;
const didMovePiece = this.props.puzzle !== puzzle && previousMove !== null;
if (didMovePiece) {
await this.updateSquarePosition(
puzzle,
previousMove,
getIndex(puzzle, previousMove),
);
}
}
Great! We’ve finished the touch event handling.
Try it out!
Save Board.js. After the app reloads, press the start button to begin the game and try dragging
pieces around.
Gestures 459
You should see the pieces smoothly scale up and down as you touch and release them. You should
only be able to drag pieces into the adjacent empty square on the board. The pieces should snap either
to their new positions or back to their original positions, but should never get stuck in-between.
Wrapping up gesture handling
We just built a drag-and-drop gesture from scratch! Let’s review what we did:
We used a PanResponder to interact with the gesture responder system. We implemented
the onStartShouldSetPanResponder handler to request the global interaction lock, and then
we implemented onPanResponderGrant, onPanResponderMove, onPanResponderRelease, and
onPanResponderTerminate to keep track of the state of the gesture.
We created a generic Draggable component that provides us with a simpler interface for han-
dling the gesture data we care about when building drag-and-drop interactions: onTouchStart,
onTouchMove, and onTouchEnd.
We handled touches by looking at the offset from the first touch event to the current one,
and deciding whether or not to move the puzzle piece. We animated the scale of the puzzle
piece so that it feels like we’re lifting the piece off of the board. We animated the position of
the puzzle piece using Animated.spring and Animated.parallel to snap it to a valid position
on the board.
Gestures 460
If a new game state was passed in as a prop, we animated the pieces to reflect the latest state
of the game board.
Putting it all together, we were able to achieve an intuitive-feeling, cross-platform drag-and-drop
gesture that performs smoothly even on lower-end devices.
Here are a few recommendations for building gestures:
Most gestures should use the PanResponder like we did in this example, rather than using
the underlying View responder props directly (e.g. onStartShouldSetPanResponder instead of
onStartShouldSetResponder). This is because most gestures will need to use the values in the
gestureState provided by the PanResponder, which are tricky to calculate on your own.
It’s often helpful to separate gesture-related code into a separate component like we did with
our Dragging component. This ensures we keep the state of the gestures (e.g. offset) separate
from the state of the board. It can also help with performance: the Draggable component will
re-render each time its state updates, but this is significantly less costly than if we had to re-
render a more complex component like the Board.
If possible, test your gestures on different devices as you build them. You may find that your
intuition about what should perform better isn’t actually the case, and it’s much easier to work
through performance issues one-at-a-time in isolation, rather than all at once when the gesture
is finished. Furthermore, if you run into a React Native bug or inconsistency across platforms
(there are several of these), it’s better to know sooner than later.
Speaking of performance, there’s one more performance improvement we made which we haven’t
covered yet.
Using PureComponent
You may have noticed that our Board component extends React.PureComponent rather than
React.Component. Components that extend React.Component will re-render any time their parent
re-renders or any time setState is called internally, even if the value of state or props is the same.
By contrast, components that extend React.PureComponent will only re-render when state or props
actually change. React checks for changes using a “shallow” equality test (testing the top-level keys
and values within the state and props objects using ===). If state and props are unchanged, then
the component does not re-render.
Preventing re-rendering with PureComponent is especially important for our Board component. Each
time the Game increments the elapsed time counter in the top right, it will re-render all of its children,
including Board. If the Board re-renders every second, this means it will likely re-render during a
drag gesture. Re-rendering results in a noticeable stutter during the gesture. In other words, we use
PureComponent to achieve a smooth drag gesture even as other parts of the UI update.
Gestures 461
If you want to see this for yourself, find this line at the top of Board.js:
export default class Board extends React.PureComponent {
Change it to:
export default class Board extends React.Component {
Then save Board.js. You’ll notice the stutter as you drag pieces around, especially on slower devices.
Make sure to change this back when you’re done!
If we had to re-render the Board during drag gestures for some reason, we might instead consider
breaking the component into smaller components, some of which can re-render more quickly or can
extend PureComponent.
Finishing the game
We’ve nearly finished building the puzzle game now. The next part we’ll write is the animation
where the pieces fly onto and off of the board as it mounts and unmounts:
Gestures 462
Animating all pieces
Let’s start by updating the constructor of the board. Open Board.js if you don’t already have it
open.
We want the puzzle pieces to render off-screen to begin with. This will let us animate them into
view. We can use the Dimensions API we learned about in the chapter “Core APIs, Part 1” to get the
height of the screen, and we can use this when setting the initial Animated.Value for the piece’s top.
Update the constructor to:
puzzle/components/Board.js
constructor(props) {
super(props);
const { puzzle: { size, board } } = props;
this.state = { transitionState: State.WillTransitionIn };
this.animatedValues = [];
const height = Dimensions.get('window').height;
Gestures 463
board.forEach((square, index) => {
const { top, left } = calculateItemPosition(size, index);
this.animatedValues[square] = {
scale: new Animated.Value(1),
top: new Animated.Value(top + height),
left: new Animated.Value(left),
};
});
}
With this, pieces will initially render exactly one screen-height below where we’ll move them.
Next, we need to animate every piece onto the board. We’ll write a helper method animateAllSquares(visible)
that does this for us. This method will animate pieces onto the board when we call it with visible
set to true, and it will animate pieces off of the board when we call it with visible set to false.
To perform the animation, we’ll animate the top of each piece by mapping the puzzle.board
to an array of Animated.timing animations (although remember that we’re eventually rendering
translateY instead of top, for performance). Just like with our updateSquarePosition method, we’ll
use Animated.parallel to run all the animations at once and let us easily await their completion.
We can create a playful staggered animation by setting the delay option of the animation.
Add the following animateAllSquares method:
puzzle/components/Board.js
animateAllSquares(visible) {
const { puzzle: { board, size } } = this.props;
const height = Dimensions.get('window').height;
const animations = board.map((square, index) => {
const { top } = calculateItemPosition(size, index);
return Animated.timing(this.animatedValues[square].top, {
toValue: visible ? top : top + height,
delay: 800 * (index / board.length),
duration: 400,
easing: visible ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
useNativeDriver: true,
});
});
Gestures 464
return new Promise(resolve => Animated.parallel(animations).start(resolve));
}
Notice that depending on the visible argument, we either animate the piece to its position on the
board, top, or off the board, top + height.
Let’s now update our componentDidMount to call animateAllSquares:
puzzle/components/Board.js
async componentDidMount() {
await this.animateAllSquares(true);
const { onTransitionIn } = this.props;
this.setState({ transitionState: State.DidTransitionIn });
onTransitionIn();
}
With that, we’ve completed the animation where the pieces fly onto the board! The last thing we
need to hook up is the animation where the pieces fly off the board when the game finishes.
We can do this by updating our componentWillReceiveProps to watch for when the teardown prop is
set to true by the Game. When the teardown prop first becomes true, we’ll call this.animateAllSquares(false).
Once this completes, we’ll set the transitionState to DidTransitionOut, and we’ll call the
onTransitionOut prop to inform that Game that the Board animations are finished and it’s ready
to be unmounted.
Update componentWillReceiveProps to:
puzzle/components/Board.js
async componentWillReceiveProps(nextProps) {
const { previousMove, onTransitionOut, puzzle, teardown } = nextProps;
const didMovePiece = this.props.puzzle !== puzzle && previousMove !== null;
const shouldTeardown = !this.props.teardown && teardown;
if (didMovePiece) {
await this.updateSquarePosition(
puzzle,
previousMove,
getIndex(puzzle, previousMove),
);
}
Gestures 465
if (shouldTeardown) {
await this.animateAllSquares(false);
this.setState({ transitionState: State.DidTransitionOut });
onTransitionOut();
}
}
Try it out!
Save Board.js. After the app reloads, press the start button to begin the game. To test the animation
we just added, you can either complete the puzzle… or you can press quit. The pieces should fly
offscreen, then the screen should transition out, and finally you’ll be taken back to the start screen.
From the start screen, you can begin another game. The state of the game completely resets after
each game.
Gestures 466
We’re Done!
We’ve built a complete puzzle game with animations and drag-and-drop gestures. Our app performs
smoothly and our animations look great on both iOS and Android.
In this chapter, we learned how to use the PanResponder in conjunction with the Animated API.
Using these together, we can build nearly any common gesture you can find in a mobile app.
We covered several ways of ensuring good animation and gesture performance:
Using transform instead of top and left to avoid costly layout calculations
Using useNativeDriver to reduce the number of messages that must be passed between the
native and JavaScript threads
Using PureComponent to prevent unnecessary component re-rendering
In the next chapter, we’ll cover how to publish your app on the App Store and Play Store.
Native Modules
What are native modules?
So far in this book we’ve written all of our apps purely in JavaScript. We’ve used the built-in React
Native components and APIs to interact with the underlying native iOS and Android platforms.
However, sometimes we want to use native functionality that isn’t provided out-of-the-box by React
Native. In these cases, we can write native components and APIs ourselves, and expose bindings to
use them from JavaScript. In React Native, these bindings are called a “bridge.
Common use cases
Native modules are most commonly used for bridging existing native functionality into JavaScript.
Native modules are also occasionally used for performance.
Here are a few of the most common cases:
Accessing native platform features that React Native doesn’t support out-of-the-box, e.g.
payments APIs
Exposing components and functionality when adding React Native to an existing native app
Using existing iOS and Android libraries, e.g. authentication libraries for a 3rd party service
High-performance algorithms like image processing that are usually low-level and multi-
threaded
Extremely high-performance views when running into performance issues with React Native
views (this is rare)
When to use native modules
Using native modules should be the exception, rather than the norm. It’s generally best to write
views, algorithms, and business logic in JavaScript when possible. The JavaScript we write is almost
completely cross-platform, and updating to new versions of React Native is usually low effort. Native
modules, on the other hand, must be written per platform, and can be time-consuming to update
since they depend on both the native platform APIs and React Native’s APIs. Additionally, we can’t
use the convenient Expo preview app once we start working with native code we have to eject”
our app (covered later in this chapter) and build it using Xcode and Android Studio.
If we’re integrating React Native into an existing app (this is known as a “hybrid” app), it’s likely
we’ll use native modules more frequently, since we’ll want to expose the existing components
Native Modules 468
and functionality of our app to React Native. In the short term, it’s often faster to bridge existing
components than to re-write them in React Native. However, in the long term, it can be better to
re-write them by migrating components to React Native, we’ll only need to maintain a single
implementation, and our team will only need knowledge of a single language/platform.
Native modules on npm
When we decide we need a native module, we should first check if there’s an existing open source
implementation. We’ll likely find an npm package for common use cases such as taking photos,
playing videos, and displaying maps.
The GitHub organization react-native-community
86
maintains several of the most popular
native modules. These modules are very high quality and maintained by React Native core
contributors.
It’s very important to read the installation instructions, as setup for native modules can vary.
Most native modules on npm come with two sets of instructions, one for automatic setup using
react-native link, and one for manual setup.
react-native-link
Most of the time, installing a native module consists of 2 steps:
1. Install the npm package with: yarn add foo
2. Integrate the native code into your app by running react-native link
Remember, yarn and npm work interchangeably, but you should always stick to one or the
other. Because we’re using yarn in this book, if you see npm install foo in a package’s
installation instructions, make sure to run yarn install foo instead!
The command react-native link can often integrate native modules automatically. Library authors
can configure the various paths and settings used by this command to integrate their native code.
However, react-native link only handles the most common cases, so many native modules come
with custom setup instructions beyond this. Custom setup instructions usually involve manually
modifying iOS and Android native code.
86
https://github.com/react-native-community
Native Modules 469
Manual setup
If you’re building a hybrid app, it’s likely your directory structure and code will differ somewhat
from the structure expected by react-native link. For this reason, native modules usually
include a set of instructions for manually integrating the native code into your app. This generally
involves modifying the Xcode and gradle build configurations to compile native libraries that were
downloaded by yarn into the node_modules directory.
The react-native link command supports some configuration options by adding an rnpm:
{ ... } object to your project’s package.json. However, documentation is currently non-
existent. If you choose to try this, the source code for reading configuration options is
currently here
87
.
Building a native module
In this chapter, we’ll build an app that displays a native pie chart:
87
https://github.com/facebook/react-native/blob/6942408a474af80560497193f73c9e60bf272263/local-cli/core/ios/index.js#L34
Native Modules 470
There are a variety of graphing libraries available for React Native already, some of which are written
in JavaScript and some of which are native modules. In order to explore how native modules work,
however, we’ll write a native pie chart module from scratch.
The semantics of native iOS and Android code are outside the scope of this book, so we will primarily
copy and paste the native code we need. People without any experience writing native code are often
able to bridge simple native modules, so we recommend you attempt to follow along even if you
don’t have any experience with these platforms.
Building this app will consist of the following steps:
1. Create a new app using create-react-native-app
2. Eject the app we just created so that we can modify its native code
3. Write the pie chart component for both iOS and Android
4. Create a single PieChart.js that renders the native pie chart component from JavaScript
If you’re primarily testing on Android, feel free to skip the Xcode/iOS sections, or vice
versa. The project will work correctly on one platform regardless of any native code or
development tools for the other platform.
Native development is challenging!
There are a lot of things that can go wrong when developing a native app. Although the code
we’ll write in this chapter is relatively simple (as native apps go), it’s likely you’ll run into several
challenges along the way, especially if you’ve never done native development before.
The most challenging issues tend to be related to your development environment or build tools.
These can be tricky to debug, since they may be somewhat unique to the setup on your computer.
When you encounter an error with a development tool or building the app, the best place to start
is with a Google search. This will often reveal a Stack Overflow question or GitHub issue where
somebody else in the community had the exact same problem. If you don’t find anything useful, we
recommend opening a GitHub issue on the React Native github repo
88
. This is the most likely way
to have your issue resolved in a timely manner. If that still doesn’t work, you’re welcome to ask us
(the authors) for help (instructions on how to do so are in the introduction chapter), but be aware
that it’s unlikely we will be able to solve problems related to native app development.
It’s not all bad news though! The React Native community is extremely active, and new native
modules are added to npm frequently writing custom native modules will become less and less
common as the ecosystem evolves. These complex challenges with native development are a big
reason for React Native’s success after all!
88
https://github.com/facebook/react-native
Native Modules 471
Development environment
Before we can get started building the pie chart app, you’ll need to set up your development
environment. If you haven’t done native app development before, it’s likely you’ll need to download
some new software. Building for iOS will require a computer running macOS with Xcode installed.
Building for Android can be done on any computer with Android Studio installed. We recommend
you set up at least one of these tools before continuing with this chapter.
To set up your development environment, follow the instructions on the “Getting Started”
89
page of
the React Native docs site. First click the “Building Projects with Native Code” tab at the top of the
page, then select the “Development OS” and “Target OS” you plan to test with.
Follow the instructions for the “Development OS” and “Target OS” of your choice, up until the
section “Creating a new application. We’ll create a new application in a slightly different way than
these docs demonstrate (although both will give the same result).
89
https://facebook.github.io/react-native/docs/getting-started.html
Native Modules 472
Initializing the project
Just as we did in the previous chapters, let’s create a new app with the following command:
$ create-react-native-app pie-chart --scripts-version 1.14.0
Once this finishes, navigate into the pie-chart directory.
Ejecting
Throughout this book, we’ve been using Create React Native App (CRNA) to set up new React Native
projects. This tool dramatically speeds up the process of creating a new project and previewing it
on your mobile device. By using the Expo client app for previewing, CRNA eliminates the need to
compile any native code. The Expo client app already contains a compiled version of all of the React
Native framework code, so apps written purely in JavaScript can be previewed within the Expo
client app by executing a JavaScript code bundle downloaded over the network.
However, as soon as we want to add custom native code, we won’t be able to preview our app using
the Expo client app. We’ll need to build our native code using Xcode and/or Android Studio. CRNA
can facilitate converting a pure JavaScript app into an app with both JavaScript and native code
through a process called ejecting”. Ejecting a project copies the necessary native build dependencies,
Native Modules 473
configuration files, and scripts into the app’s root directory. After ejecting, we’ll be able to build
custom native code in our app, but we’ll no longer be able to use the Expo client app for easy
previewing.
Since there’s no way to undo an eject, if you’re ejecting an existing project, make sure the project is
backed up in a version control system. In this case, our pie-chart project is brand new, so backing it
up is optional.
From the root of the pie-chart directory, run yarn run eject to eject the project now. Upon running
this command, you should see the following prompt:
1 We didn't find any uses of the Expo SDK in your project, so you should be fine to ej\
2 ect to
3 "Plain" React Native. (This check isn't very sophisticated, though.)
4
5 We strongly recommend that you read this document before you proceed:
6 https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md
7
8 Ejecting is permanent! Please be careful with your selection.
9
10 ? How would you like to eject from create-react-native-app? (Use arrow keys)
11 � React Native: I'd like a regular React Native project.
12 ExpoKit: I'll create or log in with an Expo account to use React Native and the Ex\
13 po SDK.
14 Cancel: I'll continue with my current project structure.
There are two different ways we can eject a project: either with or without ExpoKit.
If we’re importing any of the Expo APIs from within our app (e.g. import { Constants, MapView,
LinearGradient } from 'expo';), then we may want to consider ejecting with ExpoKit (the second
option). This allows us to continue using the Expo APIs.
If we’re not importing any Expo APIs, then we don’t need ExpoKit. That’s the case here, so we
should choose the first option and eject to a “regular React Native project. The yarn run eject
command attempts to identify whether or not we’re using any Expo APIs and warn us here, the
first line of the prompt indicates that our app doesn’t use any.
If we decide we want ExpoKit at a later time, we can add it to our app then.
Select the first option, React Native: I'd like a regular React Native project.
The next prompt will ask how you want to name your app:
Native Modules 474
1 We have a couple of questions to ask you about how you'd like to name your app:
2 ? What should your app appear as on a user's home screen?
Type “PieChart” and hit enter to proceed.
The last prompt will ask how to name the Xcode and Android Studio project files.
1 ? What should your Android Studio and Xcode projects be called? (piechart)
Type “PieChart” and hit enter to proceed.
The script should then proceed with the ejection. After a few minutes (although it can take 10 or
more), you should see the following message:
1 Ejected successfully!
2 Please consider letting us know why you ejected in this survey:
3 https://goo.gl/forms/iD6pl218r7fn9N0d2
This means the project was ejected successfully!
Since we’re just building an example project, don’t fill out the survey!
There’s another tool for starting a new React Native project called react-native init. This was
the original way to get started with React Native, before create-react-native-app was created.
Using create-react-native-app is convenient, since you don’t need to have a full development
environment set up for both iOS and Android native apps.
Creating a new app with create-react-native-app then running yarn run eject is more or less
equivalent to running react-native init. If you know in advance that you’ll need to eject your
app to use native modules, then you should consider running react-native init for simplicity. We
demonstrated the ejection flow since we’ve been using create-react-native-app throughout this
book.
The “Getting Started” page of the React Native docs demonstrates how to use react-native init.
To learn more, continue reading from the “Creating a new application” section we recommended
you stop at earlier.
Project structure
Let’s take a look at the files in our project directory now:
Native Modules 475
├── App.js
├── App.test.js
├── README.md
├── android
├── app.json
├── index.js
├── ios
├── node_modules
├── package.json
└── yarn.lock
Most of the files are the same as usual, but the ios and android directories and the index.js file are
new.
The ios directory contains an Xcode project and the android directory contains an Android Studio
project. From this point on, we’ll need to build the project in either Xcode or Android Studio before
we’re able to preview it in a simulator or on a device.
The index.js file is the “entry point” of our app now in other words, it’s the first JavaScript file
in our app that gets executed when our app launches. Let’s look at this file now.
Open up index.js. You should see a call to AppRegistry.registerComponent. This registers a “root
component” of our app that will be instantiated by native code. Apps can have multiple root
components, each with a unique name, which can be instantiated within native code.
For apps created with create-react-native-app, the App.js file is normally the entry point
and the App component is registered automatically so long as it has export default in front
of it.
In this case, our root component will be named “PieChart”. Due to a bug in create-react-native-app,
the registered component name may not have the correct letter casing. If AppRegistry.registerComponent
is called with “piechart” (in lower case), replace it with “PieChart” now:
pie-chart/index.js
1 import { AppRegistry } from "react-native";
2 import App from "./App";
3 AppRegistry.registerComponent("PieChart", () => App);
Make sure to change the case of “piechart” to “PieChart” or the app will launch with an error
later! The error will say that no component named “PieChart” was registered.
Native Modules 476
How native modules work
There are 2 kinds of native modules:
API modules
UI component modules
API modules expose bindings for native methods to be called from JavaScript. When calling a native
method from JavaScript, any values passed are marshalled
90
on the JavaScript side and unmarshalled
on the native side. All APIs called from JavaScript are asynchronous, so we will need to use promises,
callbacks, or events if we want to handle a response from the native side.
UI component modules expose a new React component that we can render from our JavaScript code.
When we render this component in our JavaScript, the native thread will use a “View Manager” to
create a new native view. The View Manager handles the lifecycle of the native view, including:
instantiating the view, marshalling and unmarshalling the component’s props, updating the view
with its props, and reusing native views where possible (for performance).
Prop types
On both platforms, we’ll want our view to consume the same props. This will allow us to create a
single React component that works for both iOS and Android. Our pie chart component will use the
following props:
pie-chart/PieChart.js
12 static propTypes = {
13 data: PropTypes.arrayOf(
14 PropTypes.shape({
15 value: PropTypes.number,
16 color: ColorPropType
17 })
18 ).isRequired,
19 strokeWidth: PropTypes.number,
20 strokeColor: ColorPropType,
21 ...ViewPropTypes
22 };
The different segments of the pie chart, the data prop, are passed as an array of objects containing a
numeric value and a color string. The segments of the pie chart may optionally be rendered with a
colored stroke, configurable with a numeric strokeWidth and strokeColor string. We’ll also allow
a style prop, just like other built-in React Native components.
90
https://en.wikipedia.org/wiki/Marshalling_(computer_science)
Native Modules 477
We’ll now build a UI component native module for each platform. As we build the module, we’ll
make sure it supports the data, strokeWidth, and strokeColor props. The style prop will be handled
automatically for us.
Feel free to follow the instructions for just iOS or just Android, and come back to the other
platform another time.
iOS
The structure of the ios directory looks like this:
1 ios/
2 ├── PieChart
3 ├── AppDelegate.h
4 ├── AppDelegate.m
5 ├── Base.lproj
6 └── LaunchScreen.xib
7 ├── Images.xcassets
8 ├── AppIcon.appiconset
9 └── Contents.json
10 └── Contents.json
11 ├── Info.plist
12 └── main.m
13 ├── PieChart-tvOS
14 └── Info.plist
15 ├── PieChart-tvOSTests
16 └── Info.plist
17 ├── PieChart.xcodeproj
18 ├── project.pbxproj
19 └── xcshareddata
20 └── xcschemes
21 ├── PieChart-tvOS.xcscheme
22 └── PieChart.xcscheme
23 └── PieChartTests
24 ├── Info.plist
25 └── PieChartTests.m
We’ll be opening PieChart.xcodeproj in Xcode, and then adding a few new files to the PieChart
directory from within Xcode. We won’t be adding native tests or configuring our app for Apple TV,
so we can ignore the PieChart-tvOS, PieChart-tvOSTests, and PieChartTests directories.
Native Modules 478
Swift or Objective-C?
When working with iOS, we have two choices for which language we want to use: Swift or
Objective-C (abbreviated Obj-C). The React Native framework for iOS is written in Obj-C, so the
built-in native modules and most of the documentation uses Obj-C. However, Swift is significantly
easier to learn and use, so we’ll be using Swift. In general we recommend using Swift for new native
modules, unless you know that you need Obj-C for some reason. Regardless of which we choose,
we’ll still need to use Obj-C for one part of the process.
Exporting the native view
There are 4 things we need to do in native code to create a new UI component native module:
1. Define the view that we want to instantiate from JavaScript. This will be a subclass of UIView.
In our case, we’ll call this class PieChartView.
2. Create a bridging header to expose our Swift code to React Native (this is only necessary if
we’re using Swift). Xcode will help us create this automatically when we create our first Swift
file.
3. Define the View Manager that will handle the lifecycle of our component. This will be a
subclass of RTCViewManager. We’ll call our subclass PieChartManager.
4. Export our custom View Manager and our component’s props to JavaScript using macros. This
is done in Obj-C, regardless of whether we’re using Swift for our component or not.
Creating files
Let’s begin by creating all the Swift and Obj-C files we’ll need within Xcode. Open PieChart.xcodeproj
in Xcode now. You can do this by launching Xcode and then choosing File > Open... from the
menubar.
This project was written in Xcode 9.3 with Swift 4.1. The project will likely fail to build in older
versions of Xcode!
First we’ll create our native view class, PieChartView.swift. In the project navigator on the left,
right click the “PieChart” group (the one with the yellow folder icon) and choose “New File….
Native Modules 479
Select the “Swift File” option.
Click “Next” and then save the file as PieChartView (the file extension is added automatically) in
Native Modules 480
the ios/PieChart directory:
Click “Create. Upon creating this file, Xcode will prompt us to create a “bridging header” this
is necessary to expose our Swift code to React Native, as React Native is written in Obj-C. Click
“Create Bridging Header”:
Native Modules 481
Next, right click the “PieChart” group in the project navigator again and create the file PieChartManager.swift
following the same process. Xcode won’t prompt you to create a bridging header this time, since we
already have one.
Last, we’ll create an Obj-C file to expose our view to JavaScript. Once again, right click the “PieChart”
group and choose “New File…. This time, select “Objective-C File”:
Native Modules 482
Save this file as “PieChart” in the ios/PieChart directory.
At this point, the file navigator should show these files:
Now that we’ve created the files we need, let’s fill them out one by one.
Note that despite how the Xcode file navigator looks, PieChart-Bridging-Header.h is
actually in the ios directory, not the ios/PieChart directory. The Xcode file navigator
doesn’t map directly to the file system. There’s no need to move this file though it will
work correctly regardless of where it exists on the file system. In fact, moving files managed
by Xcode can be fairly tricky, so for our pie-chart app, we recommend against moving any
files if possible.
Native Modules 483
You may notice that new files created in Xcode include a copyright header automatically. The
copyright header for projects created with CRNA is set to “Facebook” by default, e.g:
1 // Copyright © 2018 Facebook. All rights reserved.
If you’re working on a real project, you’ll likely want to change the default value to your name or
your organization’s name, rather than “Facebook”.
The following image demonstrates how to change the organization name used for the copyright
header:
Bridging header
We’ll be copying code from the sample code directory, pie-chart/ios, into the files we just created.
Copy the contents of PieChart-Bridging-Header.h from the sample code directory into your own
PieChart-Bridging-Header.h:
Native Modules 484
pie-chart/ios/PieChart-Bridging-Header.h
1 //
2 // Use this file to import your target's public headers that you would like to expo\
3 se to Swift.
4 //
5
6 #import "React/RCTViewManager.h"
This exposes the RCTViewManager.h headers to our Swift code so that we can write a native view in
Swift.
PieChartView
We’ll be using a subclass of UIView that draws a pie chart based on the data, strokeWidth, and
strokeColor props.
Copy the contents of ios/PieChart/PieChartView.swift from the sample directory into your own
PieChartView.swift file. We’ll look at two important details of this code.
If you don’t fully understand the explanations of the native code, that’s fine! This is mainly
included for people who do have a little native iOS development experience.
Props must be member variables of the class, exposed to Obj-C using the @objc annotation, for
example:
@objc var strokeWidth: CGFloat = 0.0
When a prop updates, we need to re-draw the pie chart. We can do this by overriding the didSetProps
method of our view:
override func didSetProps(_ changedProps: [String]!) {
setNeedsDisplay()
}
PieChartManager
Now that we have our view class, we need to create a manager class to instantiate it. Copy the con-
tents of PieChartManager.swift from the sample directory into your own PieChartManager.swift
file.
This class allows React Native to instantiate our PieChartView class as needed when we render it
from JavaScript.
Native Modules 485
Export macros
Last, copy the contents of PieChart.m from the sample directory into your own PieChart.m file.
pie-chart/ios/PieChart/PieChart.m
1 //
2 // PieChartExport.m
3 // PieChart
4 //
5 // Created by Devin Abbott on 6/3/18.
6 // Copyright © 2018 Fullstack. All rights reserved.
7 //
8
9 #import "React/RCTViewManager.h"
10
11 @interface RCT_EXTERN_MODULE(PieChartManager, RCTViewManager)
12
13 RCT_EXPORT_VIEW_PROPERTY(data, NSArray)
14 RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
15 RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
16
17 @end
This file uses macros to expose the view manager class and the view’s props to React Native.
The RCT_EXTERN_MODULE macro exposes our PieChartManager class to React Native. We can now
consume a native module called PieChart (without the Manager suffix) from our JavaScript.
The RCT_EXPORT_VIEW_PROPERTY macro exposes the member variables on our PieChartView class
as props to React Native. We also provide the types of these props so they can be marshalled and
unmarshalled correctly.
You may see an error 'React/RCTViewManager.h' not found on the line with the #import
"React/RCTViewManager.h". Xcode should find this dependency during the build process, so
the error should disappear once you build the app in the next step.
Try building!
We won’t be able try our component until we render it from JavaScript, but we can at least confirm
that our Xcode project builds successfully.
In the top left of Xcode:
Native Modules 486
1. Choose a simulator to build the project on. It’s more difficult to build on a real device, so we
recommend using the simulator for this chapter unless you’re already set up for building to
your device.
2. Click the play button to start the build.
Xcode will open a new terminal and start the packager in the root directory of our app if you don’t
have a React Native packager process running on your computer. This is for convenience you may
also quit the terminal that Xcode opened and launch a packager process of your own with yarn
start as usual.
If everything goes well, after a minute you should see a “Build Succeeded” popup from Xcode. The
simulator you chose before building should launch (this can take a minute or two) and you should
see the default create-react-native-app screen.
Native Modules 487
Wrapping up iOS
We’ve finished creating a native pie chart component for iOS! The next step will be to consume it
from JavaScript. We’ll do that in the JavaScript section of this chapter.
At this point, you can either build the Android version of the pie chart, or skip ahead to the JavaScript
section (recommended) and come back to Android later.
Android
The structure of the android directory (excluding some deeply nested files) looks like this:
1 android/
2 ├── PieChart.iml
3 ├── app
4 ├── BUCK
5 ├── app.iml
6 ├── build
7 ├── build.gradle
8 ├── proguard-rules.pro
9 └── src
Native Modules 488
10 ├── build
11 └── generated
12 ├── build.gradle
13 ├── gradle
14 └── wrapper
15 ├── gradle.properties
16 ├── gradlew
17 ├── gradlew.bat
18 ├── keystores
19 ├── BUCK
20 └── debug.keystore.properties
21 ├── local.properties
22 └── settings.gradle
We’ll be opening the android directory in Android Studio. We’ll be adding a few new Java source files
to android/app/src/main/java/com/piechart/. We can ignore the rest of the files in this directory,
which are mainly configuration files.
Java or Kotlin
When working with Android, we have two choices for which language we want to use: Java or
Kotlin. The React Native framework for Android is written in Java, so the built-in native modules
and most of the documentation uses Java. While Kotlin is more modern and a popular choice for
new Android apps, the vast majority of Android apps today are still written in Java, so we’ll be using
Java for our pie chart example.
Exporting the native view
There are 4 things we need to do in native code to create a new UI component native module:
1. Define the view that we want to instantiate from JavaScript. This will be a subclass of
android.view.View. In our case, we’ll call this class PieChartView.
2. Define the View Manager that will handle the lifecycle of our component. This will be a
subclass of SimpleViewManager<PieChartView>. We’ll call our subclass PieChartManager.
3. Define a new “package called PieChartPackage, a subclass of ReactPackage, that instantiates
our View Manager.
4. Register our PieChartPackage at app launch from MainApplication.java.
Exporting the native view
Let’s begin by creating all the Java files we’ll need within Android Studio. Launch Android Studio
now, and choose “Open an existing Android Studio project”.
Native Modules 489
Select the android directory within the pie-chart directory we just created and press “OK. It may
take Android Studio a minute or two to configure the project.
Next we’ll create a new Java file, PieChartView.java. If you’ve never used Android studio before,
the following diagram depicts the steps to take. First, click the “Project” tab on the left of the window.
Second, choose the Android” option in the dropdown at the top of the file tree. Third, right click
the “piechart” directory. Lastly, choose “New > Java Class” from the context menu.
Native Modules 490
Within the dialog that appears, type
PieChartView
and click “OK.
Native Modules 491
Repeat this process of creating new files for two more files: PieChartManager.java and PieChartPackage.java.
Once you’ve finished, the file tree should look like this:
Now that we’ve created the files we need, let’s fill them out one by one.
Native Modules 492
PieChartView
We’ll be using a subclass of android.view.View that draws a pie chart based on the data,
strokeWidth, and strokeColor props. React Native doesn’t interact with view classes directly, so this
class could be written any way we want the member variables of the view could have completely
different names and types from our props.
Copy the contents of PieChartView.java from the sample directory into your own PieChartView.java
file.
PieChartManager
Now that we have our view class, we need to create a manager class to instantiate it. Copy the
contents of PieChartManager.java from the sample directory into your own PieChartManager.java
file.
This class allows React Native to instantiate our PieChartView class as needed when we render
it from JavaScript. The REACT_CLASS string determines the name of the component within our
JavaScript:
public static final String REACT_CLASS = "PieChart";
In this case, our component will be available as PieChart.
Methods of the
PieChartManager
handle updating the
PieChartView
to reflect the latest props. A
method can be registered to handle a specific prop by name using the @ReactProp annotation:
@ReactProp(name = "strokeWidth", defaultFloat = 0f)
public void setStrokeWidth(PieChartView view, float strokeWidth) {
view.strokeWidth = strokeWidth;
view.invalidate();
}
These annotated methods handle converting common JavaScript types to Java types. For more detail
on annotating props, check out this guide in the docs
91
.
Our PieChartView implementation overrides the draw method in order to do custom drawing, so
anytime we change a prop, we also call invalidate() to trigger a re-draw.
91
https://facebook.github.io/react-native/docs/native-components-android#3-expose-view-property-setters-using-reactprop-or-
reactpropgroup-annotation
Native Modules 493
Exporting the package
Now that we have our view manager class, we can create a ReactPackage subclass that registers it
with React Native. Copy the contents of PieChartPackage.java from the sample directory into your
own PieChartPackage.java file.
This class can register multiple native modules at once, including both API modules and UI
component modules. We register our PieChartManager using the following:
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new PieChartManager()
);
}
Lastly, we’ll register our PieChartPackage class within MainApplication.java.
The getPackages method currently looks like this:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
Update it to the following:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(), new PieChartPackage()
);
}
This registers our PieChartPackage with React Native.
Try building!
We won’t be able try our component until we render it from JavaScript, but we can at least confirm
that our Android Studio project builds successfully.
Native Modules 494
We can either build to a real device or an emulator. It’s often quicker and easier to build to a real
device if you have one (and a USB cable) handy. If not, now’s a good time to set up an emulator. In
either case, follow the section titled “Preparing the Android Device”
92
for information on how to set
up your real device or emulator. If the website shows the wrong operating system (e.g. macOS when
you’re running Windows), you may need to set the “Development OS” toggle at the top of this page
again. Follow the instructions under “Preparing the Android Device until you reach “Running your
React Native application.
At the top of Android Studio, click the play button:
Next, choose a device or emulator to build the project on and press “OK”:
92
https://facebook.github.io/react-native/docs/getting-started#preparing-the-android-device
Native Modules 495
This will build the app, install it on your device, and launch it automatically. Android Studio doesn’t
automatically launch the React Native packager, so once the app launches you should see a red error
screen explaining that it can’t load the script.
Navigate to the root directory of the pie-chart app we just created and start the packager with yarn
start as usual. Once this finishes, press the “RELOAD” button at the bottom of the red screen on
your device or emulator.
If everything goes well, you should see the default create-react-native-app screen.
Native Modules 496
JavaScript
Now that we have a native implementation of our pie chart component, we can use it from JavaScript.
Using the native view from JS
In your text editor, create a new file PieChart.js in the root of our project directory (at the same
level as App.js).
In this file, we’ll import the native pie chart component, and render it from a new React component.
It’s best to wrap a native component within another React component so that we have the ability to
modify component props before they’re passed to the native component, and so that we can control
which imperative APIs are available on the component class (although in this case we won’t use
any).
Let’s begin by importing the following at the top of PieChart.js:
Native Modules 497
pie-chart/PieChart.js
import React from "react";
import PropTypes from "prop-types";
import {
ColorPropType,
StyleSheet,
ViewPropTypes,
requireNativeComponent,
processColor
} from "react-native";
Next, we’ll define a React component called PieChart. This component will wrap the native
component which we’ll import shortly. In the PieChart component, we need to define the prop
types and default props in the exact same way that our native component expects. So, we need a
data, strokeWidth, and strokeColor prop type. As a reminder, the data prop should be an array of
objects, where each object contains a numeric value and a color string.
Even though we didn’t specify this explicitly in our native code, our native component also expects
every prop type supported by the built-in React Native View. This is the default behavior for native
components, which allows our native pie chart to work with layout and gestures just like any other
React Native core component.
Create the PieChart class and propTypes now:
pie-chart/PieChart.js
export default class PieChart extends React.Component {
static propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number,
color: ColorPropType
})
).isRequired,
strokeWidth: PropTypes.number,
strokeColor: ColorPropType,
...ViewPropTypes
};
Note that we use the spread syntax, ...ViewPropTypes, to add every prop type from View to our
PieChart component.
Next we’ll add defaultProps:
Native Modules 498
pie-chart/PieChart.js
static defaultProps = {
data: [],
strokeWidth: 0,
strokeColor: "transparent"
};
requireNativeComponent
Before we add the render method to our PieChart component, we first need to import our native pie
chart component. We can do this using the requireNativeComponent(viewName, componentInterface)
API. In this case, the viewName will be "PieChart" (since that’s how we exported it from na-
tive code), and the componentInterface will be the PieChart component we just made. The
requireNativeComponent API will use the propTypes of our PieChart (the componentInterface)
to ensure that props passed from JavaScript to native code have the correct names and types.
Let’s add the following line below our PieChart React component definition:
pie-chart/PieChart.js
const NativePieChart = requireNativeComponent("PieChart", PieChart);
We’ll be rendering NativePieChart from within the render method of our PieChart component.
Even though we’re declaring NativePieChart below the PieChart class, we’ll still be able to
use it from within the render method. This works the same way as declaring styles below
the component class.
One last thing to do before writing our render method: we want to set the background of our native
pie chart component to "transparent", since native components have a black background by default.
Let’s add a style that handles this for us at the bottom of the file:
pie-chart/PieChart.js
const styles = StyleSheet.create({
container: {
backgroundColor: "transparent"
}
});
Native Modules 499
Rendering
Now we can write the render method of our PieChart class. Within the render method, we’ll
render a NativePieChart component, propagating all of the props passed to PieChart into the
NativePieChart.
There are two props that we’ll modify before propagating them to the NativePieChart component:
data - we need to call processColor on each color within the objects in the data array. The
processColor function converts colors into a format that our native code can understand. React
Native normally handles this conversion automatically (e.g. for our strokeColor prop), but
since our colors are nested within objects/arrays, we must do it ourselves for the data array.
In order to do this, we’ll map each object in the data array to a new object containing the
converted color.
style - we want to apply our default styles.container style before any other styles in order
to clear the default background color.
Let’s add the render method now:
pie-chart/PieChart.js
render() {
const { style, data, ...rest } = this.props;
const processedData = data.map(item => ({
value: item.value,
color: processColor(item.color)
}));
return (
<NativePieChart
{...rest}
style={[styles.container, style]}
data={processedData}
/>
);
}
Save PieChart.js. That wraps up our PieChart component! Now it’s time to render this component
from App.
Native Modules 500
App
Open App.js. In this component we’ll render the PieChart with an initial set of data, and we’ll
include a button that randomizes the data.
We’ll begin by updating the imports at the top of the file:
pie-chart/App.js
import React from "react";
import { AppRegistry, StyleSheet, Text, View, Button } from "react-native";
import PieChart from "./PieChart";
Next, let’s add a style called chart in the styles object at the bottom of this file. This style will
render our pie chart as a 300x300 square with a margin below it. Update the styles object now:
pie-chart/App.js
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
},
chart: {
width: 300,
height: 300,
marginBottom: 20
}
});
Now let’s move on to the App component. We’ll add a component state object containing some
initial data to render in the pie chart:
Native Modules 501
pie-chart/App.js
state = {
data: [
{ value: 12, color: "#2196F3" },
{ value: 12, color: "#8BC34A" },
{ value: 8, color: "#f44336" },
{ value: 4, color: "#FF9800" }
]
};
We’ll also add a randomize function to this class which randomizes the pie chart’s data:
pie-chart/App.js
randomize = () => {
const { data } = this.state;
this.setState({
data: data.map(slice => ({
value: Math.random() + 0.1,
color: slice.color
}))
});
};
Finally, we can update the render method. Delete the current placeholder contents within the render
method. Then, render a PieChart component using the styles and data we just defined, along with
a Button below it that calls this.randomize when pressed.
pie-chart/App.js
render() {
const { data } = this.state;
return (
<View style={styles.container}>
<PieChart
style={styles.chart}
strokeColor={"white"}
strokeWidth={4}
data={data}
/>
<Button title="Press to randomize" onPress={this.randomize} />
Native Modules 502
</View>
);
}
Save App.js.
Try it out!
You’ll likely need to manually reload the app on your simulator or device, since live reload and hot
reload are disabled by default in ejected apps.
To reload:
On a physical device, shake the device until the developer menu appears, then tap “Reload”.
On an iOS simulator, press Cmd+R.
On an Android emulator, press R twice.
Once the app has reloaded, you should see the pie chart on the screen!
Try tapping the “Press to randomize button a few times to make sure the native component updates
correctly whenever props change.
Native Modules 503
Wrapping up
We just created a native module for both iOS and Android, and bridged it into React Native.
Although bridging a native module is more complex than creating a JavaScript API or UI component,
it can be necessary in order to leverage native functionality.
As the React Native community has evolved, many native modules have been published on npm.
Creating native modules manually like we did in this chapter is already significantly less common
than it was several years ago. In the future, more and more apps will likely be built without writing
any native code. However, knowing how to bridge a native module will always be an important skill
for a React Native developer, since the need might arise throughout the lifetime of a project.
In the next chapter, we’ll cover how to publish our apps to the App Store and Play Store.
Building and publishing
After spending some time completing a mobile application, the next natural step is to share your
work with the world! If we want any user to be able to access our application at any time, we need to
build and publish it to an app store. By doing so, we let users discover and download our application
to their device.
How to read this chapter
This chapter serves as a reference for when you need to deploy and distribute a React Native
application. Unlike other chapters, you won’t be following along with an example application. Feel
free to read it now to get an idea of the process and return later when you’re ready to publish an
application of your own.
Shipping an application to an app store is generally a two-step process:
1. Create a native build of our application. This involves generating an IPA (iOS App Store
Package) file for iOS and an APK (Android Package Kit) file for Android.
2. Publish the build to an app store. App Store Connect
93
and Google Play Console
94
are the two
platforms used to publish and distribute an iOS and Android application respectively.
We’ll start with Building. There are two different approaches to creating a build. We’ll weigh their
advantages and disadvantages.
After this section, we suggest you head directly to the section covering the operating system that
you plan on publishing with: iOS or Android.
Building
We explored how to add native components to a React Native application in the “Native Modules”
chapter. While doing so, we described how to set up the necessary integrated development
environments (IDEs) needed for writing native code. To develop for iOS, Xcode
95
is the required
IDE and can only be installed on a Mac computer. Android Studio
96
is the official IDE used for
Android development.
93
https://developer.apple.com/app-store-connect/
94
https://developer.android.com/distribute/console/
95
https://developer.apple.com/xcode/
96
https://developer.android.com/studio/
Building and publishing 505
In order to submit to an App Store, we first need to create a build of our application that we can
publish. If we were building mobile applications without React Native, we would use the same IDEs
that we write native code with to create builds of our application. With CRNA however, we can
create standalone builds in two different ways:
1. Using Expo
2. Ejecting and creating builds manually through Xcode or Android Studio
Both approaches will allow us to generate iOS and Android builds that we can deploy to app stores.
Each approach has its advantages and disadvantages, which we will cover in a little bit.
In the next section, we’ll explore how Expo allows us to create standalone builds using one of its
tools. After that, we’ll review the pros and cons for using this approach as well as the trade-offs for
building manually using the IDEs.
Building with Expo
In the first chapter, we mentioned that Expo provides a number of tools
97
to simplify the process of
building and publishing React Native applications without using Xcode or Android Studio. Although
we’ve used the Expo Client extensively throughout the book to run and demo our projects, we have
not explored any other features provided by the platform. Expo provides two local development
tools that allow us to preview, share and publish our projects:
1. XDE
98
, or the Expo Development Environment, is a desktop app that we can use for macOS,
Windows, or Linux.
2. exp
99
is a command line interface.
XDE does not allow developers to create standalone builds to deploy to App Stores. For that reason,
we will be using the exp CLI throughout this chapter. We can begin by installing it globally:
yarn global add exp
In order to use any of the commands or services provided by exp, you need to have an Expo account.
You can create one at https://expo.io/signup
100
. Once your account is set up, you can sign in directly
through the command line:
97
https://expo.io/tools
98
https://github.com/expo/xde
99
https://docs.expo.io/versions/latest/workflow/exp-cli
100
https://expo.io/signup
Building and publishing 506
exp login
The app.json file is automatically generated in the root directory when creating a new project with
CRNA. The file allows us to modify a number of build configurations for our application:
{
"expo": {
"name": "Weather",
"slug": "weather",
"sdkVersion": "27.0.0",
"icon": "./path/app-icon.png",
"version": "1.0.0",
"ios": {
"bundleIdentifier": "com.companyname.appname"
},
"android": {
"package": "com.companyname.appname"
}
}
}
The attributes shown here are all the basic required fields needed to generate a native build:
name: The name of the application that shows on the device home screen for applications
installed through an App Store.
slug: The URL name for published Expo applications. For this configuration, the URL will look
like expo.io/user_name/weather.
sdkVersion: The Expo sdkVersion that the application is running with. This needs to match
with the installed version in package.json.
icon: The icon of the application that shows on the device home screen.
version: The version of your current build.
ios/android: iOS and Android build-specific configurations. The iOS bundleIdentifier and
Android package fields are both unique strings that identify the application and follow a reverse
domain naming convention. It is important to make sure a proper name is chosen for both fields,
as they cannot be changed once the application is published to an App Store.
Building and publishing 507
Although these are the only fields that are required to create native iOS and Android builds,
there are many other configurations that we can add including defining a splash screen
101
,
modifying the app status bar
102
and using appropriate and platform specific app icons
103
.
Expo provides an extensive guide
104
for best practices when creating builds to publish to
App Stores. Further, Expo provides a full list of possible configurations
105
.
Pros and cons of building with Expo
Creating builds with Expo is only possible if we do not plan on including any custom native code
whatsoever. If we need to include any native dependencies in our application, we have to eject
from CRNA and create our builds manually using Xcode and Android Studio. If you have already
ejected, then skip right ahead to the “Building Manually” section of this chapter. Otherwise, we
highly recommend to use Expo as it is a much simpler and quicker build process.
The Expo platform supplies a CLI to automate the process of creating a native iOS or Android build
of our application. When building for both iOS and Android, signing files are required to identify the
author of the application. Expo can automatically generate these files for you. This method allows
us to create iOS builds without using Xcode and without owning a Mac computer. However, Xcode
is still required to publish the build to the iOS App Store.
Another significant advantage of using this approach is over-the-air (OTA) updates. By default,
applications published with Expo will always check for updates when launched. If a new version of
the app has been published with Expo, the updated build will be fetched and loaded automatically.
With this, we can release fixes and updates to our application without going through the process
of publishing a new version to the App Store. We’ll cover this feature, along with its limitations, in
more detail at the end of this chapter.
A drawback of this approach is the limited control it allows over the build process. The final bundled
build will include every API provided by Expo, regardless of whether we use them or not. This can
result in significantly large build sizes even if the code that makes up our application is relatively
small.
Pros and cons of building manually
We can create builds of our application using the IDE for each platform, Xcode and Android Studio.
The primary advantage of this approach is the capability to add native iOS and Android code to our
application. Building manually also allows us to take full control over the build process, and this
includes modifying any signing steps and adjusting the final build size ourselves.
101
https://docs.expo.io/versions/latest/guides/splash-screens.html
102
https://docs.expo.io/versions/latest/guides/configuring-statusbar.html
103
https://docs.expo.io/versions/latest/guides/app-icons.html
104
https://docs.expo.io/versions/latest/guides/app-stores.html
105
https://docs.expo.io/versions/latest/guides/configuration.html
Building and publishing 508
The downside is that in order to use an IDE to build our app, we have to eject from Expo. This means
that we will have to own a Mac computer to create an iOS build. And as mentioned in the previous
section, the process of creating builds manually is much more complicated than using Expo. We
recommend taking this approach only if you need to include native code for your application and
own a Mac computer (if you wish to build for iOS).
As we covered in the last chapter, we have two different options for ejecting. We can either
eject to a regular React Native project, or detach to ExpoKit. Ejecting to regular React Native
means we also lose access to APIs provided by Expo. Further, we no longer benefit from the
platform’s built-in OTA updates. To push new updates to our users without deploying a
new build version, we’d need to use a tool like Microsoft’s CodePush
106
. However, ExpoKit
allows us to build with the IDEs while retaining access to Expo APIs (and OTA updates).
Reference Guide
Now that we’ve outlined our options, the rest of this chapter will be a reference guide that you can
refer to when you need to build and publish a React Native application. You can skip directly to the
operating system that you are currently working with.
iOS
As we mentioned previously, you can skip directly to the appropriate build method depending on
the state of your application:
If you have not ejected your application from CRNA and do not plan on adding any native
dependencies, you can read the next section and skip the section that explains how to use
Xcode to create a build.
If you have an ejected application, skip the following section and head directly to Creating an
iOS build with Xcode.
After using one of the two strategies to create a build, you can head to the Testing the final build
section to learn how to test your final build before continuing to read how to publish to the App
Store.
Creating an iOS build with Expo
In order to create builds for iOS applications, you need to enroll in the Apple Developer Program
107
.
You can either enroll as an individual or an organization. As an individual, apps distributed in the
App Store are tied to your personal name. As an organization, apps are tied to a legal entity’s name.
106
https://github.com/Microsoft/react-native-code-push
107
https://developer.apple.com/programs/enroll/
Building and publishing 509
Once we have a developer account set up, we can build a native iOS bundle through Expo with the
following command:
1 exp build:ios
We are then asked how we would like to handle our credentials:
The credentials here refer to signing certificates and a provisioning profile needed to submit
applications to the App Store. We’ll go with the simpler option of letting Expo handle all of the
credentials here, but we will explore what each of these mean in detail in the next section.
Once we have selected the option for Expo to take care of managing our files, we’ll need to submit
the ID and password for our Apple Developer Account:
Although we specified we want Expo to handle all credentials, we still have the option to provide
overrides for specific certificates. Expo prompts us about these next. We want Expo to handle both:
Building and publishing 510
Next, the build process will begin and a URL is provided (e.g. https://expo.io/builds/unique-id).
This URL provides access to the current status of the build and you can follow along by reading its
logs.
Clearing Credentials
Expo will always check if a certificate and provisioning profile exist before asking whether
it should handle creating new files or allow you to provide them. If you wish to clear the
already available credentials, you can run exp build:ios --clear-credentials.
The build process can take a few minutes to complete. Once completed, another URL will be provided
that contains the generated .ipa build file. Pasting this link into your browser’s address bar will
begin downloading it to your machine.
Creating an iOS build with Xcode
As we mentioned earlier in this chapter, an account with the Apple Developer Program is necessary
in order to create a build ready for distribution. You’ll need to enroll
108
if you haven’t already.
Once enrolled, the first thing we’ll need to do is code sign our application. Code signing is the
process of using a digital signature to identify the application author’s identity. Signatures are used
to ensure that updates to an application are published by the same author.
With Xcode, we have two options for code signing an application:
1. Manually, where we provide all the resources ourselves
2. Automatically, where Xcode takes care of creating all the necessary signing credentials
Automatic code signing is recommended in the Xcode documentation as it simplifies the process
of creating all the required assets needed for signing. Xcode takes care of creating all the needed
credentials and we only have to provide our developer account without doing more additional work.
With this approach, Xcode will:
Create the necessary signing certificates
108
https://developer.apple.com/programs/enroll/
Building and publishing 511
Create an App ID
Handle all the provisioning profiles needed
Manually code signing an application means that we will have to create all the needed assets in the
Apple Developer Console and assign them to our application ourselves. This approach gives us some
more control over which provisioning profiles and signing certificates we would like to use.
If you don’t need to use a particular profile or certificate for your application, you should use the
recommended approach of automatic code signing. We’ll cover this first. After reading, feel free to
skip the portion of this chapter where we cover how to manually code sign your application.
If you are interested in learning more about how provisioning profiles and signing certificates work,
or you would like to manually handle these credentials yourself, then you can head directly to
Manual Code Signing.
Xcode version 9.1 is used in this chapter. There may be slight inconsistencies to the UI displayed
in the screenshots if you happen to be using a later version of the software. However, the general
procedure should remain the same.
Automatic Code Signing
To access a React Native application using Xcode, we can open the /ios/AppName.xcodeproj file in
our project. An Xcode project file (.xcodeproj) contains all the source files and resources needed
for building and managing a project with the platform.
Once the file is opened with Xcode, you should see your application loaded as the main target. This
is what the default dashboard looks like for an open application:
Building and publishing 512
The bundle identifier in the Identity section of the General tab is a required field and is a unique
string that identifies your application. Once a build of your application is uploaded for submission,
this field cannot be changed. The version and build number fields also need to be completed and
will be used by App Store Connect to identify different versions of your application.
In the Signing section of the screen, there is a checkbox for Automatically manage signing. After
checking this field, a “Team” drop-down is displayed asking for a development team that can be used
to associate all the created signing credentials. In order to see your team as one of the drop-down
options, you must add an Apple Developer Account to Xcode. You can add this account by opening
Xcode � Preferences in the main menu.
Once a development team is selected, you should see a signing certificate assigned to your developer
account. Xcode takes care of creating this certificate along with a provisioning profile and assigns it
to your team. With a certificate set up, you can now head directly to the Creating a build archive
section to create a build of your application.
Manual Code Signing
To begin the process of manually code signing, we’ll need to sign in to the Apple Developer Portal
109
.
109
https://developer.apple.com/account/
Building and publishing 513
The Apple Developer Portal provides links to resources, guides and documentation. There are two
important tools provided by the platform that we will be exploring:
Certificates, Identifiers & Profiles is where we set up signing certificates and profiles for our
application to identify them.
App Store Connect is a collection of tools that allow us to manage and submit application
builds to the App Store. We will cover how to use this to publish an application in the next
section.
Let’s navigate to Certificates, Identifiers & Profiles and begin by creating an appropriate
signing certificate. Once we are at the certificates
110
screen, clicking the + icon on the right hand of
the screen will allow you to create a new certificate. Make sure iOS, tvOS, watchOS is selected in
the dropdown in the left navbar.
110
https://developer.apple.com/account/ios/certificate/
Building and publishing 514
Signing certificates are digital signatures used to perform actions while ensuring the application
is from the same source and has not been altered with. There are two certificate types relevant to
building and distributing an iOS application:
Development (iOS App Development): Used for the development of an iOS application and
limits the number of devices that the application be installed on
Distribution (App Store and Ad Hoc): Used for the distribution of iOS applications to App
Stores or for Ad Hoc distribution
If you need to continue development of a React Native application after ejecting from CRNA and
would like to handle code signing manually, you will need to create an iOS App Development
certificate and register your device. Since this chapter focuses on distributing an application, we’ll
select App Store and Ad Hoc and click continue.
These are many more certificate types available for other use cases (such as enabling push
notifications or building and distributing a macOS app). You can see a full list of them in
the Xcode docs
111
.
111
https://help.apple.com/xcode/mac/current/#/dev80c6204ec
Building and publishing 515
The next screen provides instructions to create a Certificate Signing Request (CSR) file from a Mac
computer.
A CSR file is a formatted and encrypted file that contains information that identifies a particular
source and is used to issue a certificate. You can follow the instructions provided to save a CSR file
somewhere on your computer. Once that’s done, click continue to proceed and upload the file. Once
uploaded, you should be able to download the certificate by clicking Download. Double-clicking the
certificate file (.cer format) will save it in your Keychain.
Keychain Access
112
is a Mac application that can store sensitive account information such
as passwords and certificates.
Let’s move on to creating an app identifier. This is also known as the App ID and is a unique string
that identifies a single application (or multiple applications) connected to a development team. We
112
https://support.apple.com/en-ca/guide/keychain-access/what-is-keychain-access-kyca1083/mac
Building and publishing 516
can do this by navigating to the iOS App IDs
113
screen by clicking Identifiers/App IDs in the side
menu. To register a new App ID, we need to fill out a few fields:
App ID Description: Any description of your application can be used here, but this is usually
the same as the name of the app.
App ID Prefix: This is your Team ID by default.
App ID Suffix: By changing the suffix of our App ID, we can choose between its two different
types: * An Explicit App ID is used for a single application and must be unique. A reverse
domain naming convention is commonly used (com.companyname.appname). This option needs
to be selected in order to include most application services such as in-app purchases and push
notifications. You can see a list of all services that can be enabled/disabled at the bottom of the
screen. * A Wildcard App ID can allow a single identifier and provisioning profile to be used
for multiple apps. Many Apple services cannot be included with this type of ID. Since App IDs
cannot be changed for an application submitted to the App Store, it is important to ensure that
this option is only selected if no such services are included and will not be in the future. An
asterisk must be the last character of the ID (com.companyname.*) here.
App Services: In here, you can select any Apple services to include in your application.
Once a certificate and identifier is created, a provisioning profile needs to be set up for the
application in order to allow it to be downloaded on physical devices. A provisioning profile is
the combination of an application’s unique bundle identifier and a signing certificate. Without a
profile, we cannot run an app on a mobile device.
A distribution provisioning profile is needed when an application is ready to be distributed to
multiple users. To create a distribution provisioning profile, we can click the Provisioning Profiles
–> Distribution
114
list item in the side menu and then + on the right hand side of the screen.
113
https://developer.apple.com/account/ios/identifier/bundle
114
https://developer.apple.com/account/ios/profile/production/create
Building and publishing 517
Of the many possible options, there are two specific types of distribution profiles relevant to iOS
applications:
App Store: Used to submit an application to the App Store.
Ad Hoc: Used to distribute applications to multiple testers. Unlike a development provisioning
profile, an Ad Hoc profile can not used to debug applications.
Select App Store to create the profile you need to submit an application to the App Store. In the next
few screens, you will need to select the App ID you just created and the certificates you wish to
include in the profile. Once completed, you can name your provisioning profile and download it to
your machine.
Building and publishing 518
While creating a build of an application, a development provisioning profile uses a
development signing certificate to connect developers and devices to an authorized team.
This allows for multiple developers to debug on more than one device. The process of
selecting an App ID and certificate is the same, but each device’s unique device identifier
(UDID) needs to be explicitly added to the same profile. This can be done in the Apple
Developer Console
115
.
The final step here is to manually assign our created distribution certificate to our project in Xcode.
We can open the /ios/AppName.xcodeproj file to do this.
The bundle identifier field must be the same as the AppID you created earlier in the developer console,
so you’ll need to make sure that it matches in order to create a successful build.
To manually take care of assigning a provisioning profile to our application, disable the Automatically
manage signing checkbox. Two newer sections, Signing (Debug) and Signing (Release), will
show up underneath. Select the Provisioning Profile dropdown for our signing release and click
Download Profile to download the distribution profile directly from the Apple Developer Console.
We can do the same for the debug section if we have a development profile created as well.
115
https://developer.apple.com/account/ios/profile/
Building and publishing 519
Creating a build archive
Once all the credentials needed for code signing have been set up, we can create a build archive of
our application. We can begin by changing our device target at the top of the screen to Generic iOS
Device.
For an ejected CRNA application, we can run our application on different simulators using
the react-native run-ios --simulator="iPhone 8" command. We can also use this device
target menu on Xcode to do the same thing (as well as preview our application on a physical
device plugged in to our computer).
The Generic iOS Device target is only used to create an iOS device build. With this target selected,
we can select Product � Archive to create a build archive. After a few minutes, a window showing
all past archives will pop up. Clicking Export on the right hand side and selecting App Store as our
Building and publishing 520
method for distribution will allow us to export an .ipa file of our application. Instead of doing this
however, we can also submit directly to App Store Connect by clicking Upload to App Store. We’ll
cover the flow of this process in the next section.
Testing the final build
Since we signed the iOS build we created earlier in this chapter with an App Store distribution
certificate, it cannot be downloaded and run on a personal simulator/device for testing purposes
through Xcode. To allow for this, we can use TestFlight
116
. TestFlight is an Apple platform that allows
others to download and test a production build on their devices. Instructions for using TestFlight to
invite users to test your production build can be found on the TestFlight website
117
.
Publishing to the iOS App Store
Submitting and managing application builds for iOS can be done in App Store Connect
118
. You can
sign in with your developer account.
116
https://developer.apple.com/testflight/
117
https://itunespartner.apple.com/en/apps/videos#testflight-beta-testing
118
https://developer.apple.com/app-store-connect/
Building and publishing 521
App Store Connect provides a collection of tools that developers can use to manage their applications,
view user analytics, study trends and patterns as well as view financial results. Let’s navigate to My
Apps to view a dashboard of all the applications tied to your developer account. Nothing will show
here if you haven’t submitted an application before.
Click the + icon on the top left to add a new application. We’ll need to fill out a form with a few
fields:
Platform: Select whether you’re creating a new iOS or tvOS application.
Name: This is the display name of your application in the App Store.
Primary Language: The main language your application is localized for.
Bundle ID: The unique identifier for your application. If you let Xcode automatically code sign
your application or Expo handle creating credentials for you, you should see the ID for your
application in the dropdown. If you don’t see it, you may have to create your App ID manually
in the Developer Portal
119
.
119
https://developer.apple.com/account/ios/identifier/bundle
Building and publishing 522
SKU: This is the Stock Keeping Unit
120
, an identifier commonly used for inventory and financial
tracking. It also needs to be unique to each application, and you can use the same string as your
bundle ID if you like.
Clicking Create will create our application on the platform.
We can add more general information here such as the application’s subtitle, privacy policy link, and
categories. On the left side of the screen, you’ll notice that the current status of our application’s
submission process is 1.0 Prepare for Submission. 1.0 refers to the version of the application, and
this number will change once we upload newer build versions. Clicking the link in the side menu will
navigate to another form that allows us to provide version-specific information of our application.
This includes:
iPhone and iPad application previews and screenshots
120
https://www.investopedia.com/terms/s/stock-keeping-unit-sku.asp
Building and publishing 523
Promotional text
Keywords
App description
App Store Icon
App review information
All of this information can be modified at any time by submitting updated builds of an application.
In the Build section of the screen, you’ll see a message letting you know that you can submit builds
directly using Xcode.
Every iOS application that is published to the App Store goes through an extensive review process
before being accepted. To improve your chance of being accepted, there are a few resources you can
consider before submitting an application:
Apple’s Review Guidelines
121
Xcode’s docs on App Store distribution
122
Expo’s docs on app stores
123
In Xcode’s Window Organizer screen, you can see a list of previously created build archives of
your application. This is what the screen looks like when you have multiple builds:
121
https://developer.apple.com/app-store/review/guidelines
122
https://help.apple.com/xcode/mac/current/#/dev91fe7130a
123
https://docs.expo.io/versions/v27.0.0/distribution/app-stores.html
Building and publishing 524
Before we upload our build archive to the App Store, we can validate it to ensure that our build
files pass all the validation checks performed by App Store Connect. We can do this by clicking the
Validate button in the menu on the right. If any necessary configurations or assets are missing,
Xcode will give us an error here.
If our build archive is validated successfully, we can move on to clicking Upload to App Store. After
selecting the correct development team, we’ll see an Upload Successful message if there were no
issues with the archive.
It can take a few minutes for the build archive to show up in App Store Connect. Once it does, you
should see it in the Activity tab of the application. Moreover, the Build section of our submission
will now show a different message - Select a build before you submit your app. Clicking the
link will display a list of available builds.
After the build shows up on App Store Connect, it may still need to finish processing which
can take a little more time. At this point, you’ll see a (Processing) message next to your
build and you’ll only be able to select it for submission once it completes.
Once you select your build and complete all the necessary information for your application, you
Building and publishing 525
can submit it using the Submit for Review button. According to Apple’s support documentation
124
,
review times vary. Half of all applications submitted are reviewed within 24 hours and over 90% are
reviewed within 48 hours. The status of the application in App Store Connect will be In Review
until it is completed.
If your application requires any form of authentication in order to be used, you should also
provide sign-in information for a dummy account in App Store Connect’s submission form.
Without this information, an application may not be able to be reviewed.
Android
As we mentioned previously, you can skip directly to the build approach that is more relevant for
your application:
If you have not ejected your application from CRNA, you can read the next section and skip
the section that explains how to create a build manually
If you have an ejected application, skip the following section entirely and head directly to
Creating an Android build manually
After using the appropriate strategy to create a build, we’ll discuss how to publish your application
in the Publishing to the Play Store section.
Creating an Android build with Expo
We can start a build process for Android with the following command:
1 exp build:android
We are then asked if we would like Expo to take care of creating a keystore or uploading it ourselves:
124
https://developer.apple.com/support/app-review/#app-review-status
Building and publishing 526
A keystore is a file that contains private keys used to authenticate oneself. We’ll explore this in more
detail when we build an Android application manually later in this chapter, so we’ll go with the
option of letting Expo take care of it here.
Next, the build process begins and can take a few minutes to complete. As with the iOS build process,
a URL (https://expo.io/builds/unique-id) is provided that shows real-time logs from Android
Studio. Once the build process is finished, we’re provided a URL that contains the final .apk build
file. We can download the file by pasting the link into our browser’s address bar.
Clearing Credentials
A keystore file is used to represent the application owner’s identity, so keeping it secure is
extremely important. Moreover, we cannot submit updates to our application and publish
new versions if we lose our keystore file. We can clear a previously generated keystore used
by Expo with exp build:android --clear-credentials but it is important to make sure
we have fetched and stored a local version of the keystore file first. We can do that with exp
fetch:android:keystore.
Testing the final build after automatic signing
There are two different ways to test the final build of your application:
1. On an emulator.You can drag and drop the final build .apk to have it boot up automatically.
2. On a physical device. You will have to first install Android Platform Tools
125
to your computer.
This includes Android Debug Bridge (adb), a command-line interface that allows you to control
an Android device connected to your machine. Once installed, you can run adb install
apk-file-name.apk with your device plugged in.
After setting up and testing your final application’s build file, you can head directly to the
Publishing to the Play Store section to learn how to publish your application as well as distribute
testing versions through different channels.
Creating an Android build manually
In order to code sign an Android application with a certificate, the build process requires a keystore
that contains an app signing key. This is done to ensure the source has not changed if any updates are
done to the application. There are two different ways to create an Android keystore for an ejected
React Native application:
1. Using Java Keytool
2. Using Android Studio
125
https://developer.android.com/studio/releases/platform-tools
Building and publishing 527
Using Java’s Keytool application can be quicker and is the approach mentioned in the React Native
documentation. For that reason, we’ll explain the process of using it here. However, instructions to
create a keystore using Android Studio can also be found in the Android Studio docs
126
if you prefer
that approach.
Keytool
127
is a command-line tool already included in the Java Development Kit that simplifies the
management of keys and certificates. It can be used to create a keystore containing an app signing
key used to sign an Android build.
The Java Development Kit (JDK) is a development environment to run and develop
applications built with Java. It is required to build Android applications with React Native
and you should already have it installed after ejecting and following the instructions
128
to
build native Android code.
For Unix-like operating systems such as macOS and Linux, the keytool directory is added directly
to the $PATH variable of our system. This means we can run the command in any directory as an
executable. If you are using a Windows machine, you can only run keytool commands in C:\Program
Files\Java\jdk1.x.x_xxx\bin where 1.x.x_xxx is the version of JDK installed on your machine.
We can generate a keystore with a single key using the following command in our terminal:
$ keytool -genkey -v -keystore android-release.keystore -alias android-release-alias\
-keyalg RSA -keysize 2048 -validity 10000
The command contains a number of settings that would apply to our generated keystore:
-keystore: The name of the keystore file. In here, the file would be named android-release.keystore.
-alias: The keystore alias is its unique identifier and is used to code sign the application. Our
alias here is android-release-alias.
-keyalg: Defines the algorithm used to create a public-private key pair in our keystore. RSA is
commonly used, but you can find out about all the possible options in more detail by referring
to Java’s cryptography architecture guide
129
.
-keysize: Specifies the size of the created key. In short, key size is used in cryptography to
define the number of bits in a key used in an algorithm. In here, we’ve specified 2048 bits
(which represents 256 bytes).
-validity: The length of time our key will be valid in days. We’ve set our validity to 10000
days here.
These parameters are all we need to generate a working keystore for our application. You
can see a full list of possible configurations by typing keytool usage: to your terminal.
126
https://developer.android.com/studio/publish/app-signing#generate-key
127
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html
128
https://facebook.github.io/react-native/docs/getting-started.html#java-development-kit
129
https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html#AppA
Building and publishing 528
Running the command will then prompt for passwords for the keystore and signing key, as well as
the Distinguished Name fields for your key. Once provided, an android-release.keystore file will
be created in the current directory.
Due to the fact that a keystore represents an application owner’s identity, it is extremely important to
remember to keep a keystore file secure. If the file is lost, updates to an application in the Play Store
cannot be performed and a new app will have to be created and submitted. If the file is compromised
or stolen, an attacker can publish a newer version of your application with malicious code. It’s
considered best practice to avoid committing a keystore to your version control system, like Git.
We need to place our keystore file in the android/app directory of our project. Once that is done, we
can configure the Gradle build process by adding our keystore and key information. Properties that
modify the build process can be added to the android/app/.gradle/gradle.properties file. Let’s
add the following to the bottom of the file:
APP_RELEASE_KEYSTORE_FILE=android-release.keystore
APP_RELEASE_KEYSTORE_PASSWORD=YOUR_KEYSTORE_PASSWORD_GOES_HERE
APP_RELEASE_KEY_ALIAS=android-release-alias
APP_RELEASE_KEY_PASSWORD=YOUR_SIGNING_KEY_PASSWORD_GOES_HERE
Gradle
130
is an extensible and configurable build system used by Android to automate and
manage the entire build process. Instead of configuring Gradle to sign our application,
we also have the option of signing it manually
131
using the apksigner
132
tool provided by
Android Studio.
Since these properties will need to be referenced in our build file, you can choose any key name for
each of these key:value pairs.
130
https://gradle.org/
131
https://developer.android.com/studio/publish/app-signing#sign-manually
132
https://developer.android.com/studio/command-line/apksigner
Building and publishing 529
In the above example, we added the username and password of both the signing key and
keystore in plain text to the Gradle properties file. This can be dangerous if you are working
on a project with multiple team members or if you submit your project to an open source
platform.
In these conditions, there are a few options that can be performed to keep this sensitive
information outside of this file:
1. Create a separate keystore.properties file to contain this information which can be
accessed in the application’s build file. Instructions to set this up can be found in the
Android Studio docs
133
. The properties file should not be committed to version control
and can be added to the .gitignore file of the project so it is ignored by Git.
2. For macOS users, the credentials can be stored in Keychain Access and
loaded directly in the build file. Instructions can be found on this page:
https://pilloxa.gitlab.io/posts/safer-passwords-in-gradle/
134
.
Once our signing credentials are added to our Gradle properties, we can access them in our build file
to complete the signing process. We can apply configurations to the build process by adding them
to the android/app/build.gradle file. Default configurations are added to this file as soon as we
create a new Android project.
Let’s add a signing configuration to the android block within this file underneath our default
configurations:
android {
// ...
defaultConfig {
// ...
}
signingConfigs {
release {
if (project.hasProperty('APP_RELEASE_KEYSTORE_FILE')) {
storeFile file(APP_RELEASE_KEYSTORE_FILE)
storePassword APP_RELEASE_KEYSTORE_PASSWORD
keyAlias APP_RELEASE_KEY_ALIAS
keyPassword APP_RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
133
https://developer.android.com/studio/publish/app-signing#generate-key
134
https://pilloxa.gitlab.io/posts/safer-passwords-in-gradle/
Building and publishing 530
//. ..
signingConfig signingConfigs.release
}
}
}
We’ll now need to assign our newly created configurations as the release signingConfig of the build
process:
android {
// ...
defaultConfig {
// ...
}
signingConfigs {
// ...
}
buildTypes {
release {
// ...
signingConfig signingConfigs.release
}
}
}
And that completes our signing process! We can now navigate to the android/ directory and bundle
our application into a build:
cd android
./gradlew assembleRelease
Starting any Gradle build is done with the Gradle Wrapper
135
, a command-line tool we can use at
the root of any Android project with /gradlew task-name. The assembleRelease command will
bundle (or assemble) all the core JavaScript code into the final build. This file can be found as a
app-release.apk file in the android/app/build/outputs/apk/ directory.
Instead of adding signing information to our Gradle properties, we can also manually sign
an application using Android Studio
136
or configure the build process
137
to automatically
sign any application. Both of these approaches will have the same end result.
135
https://docs.gradle.org/current/userguide/gradle_wrapper.html
136
(https://developer.android.com/studio/publish/app-signing#release-mode)
137
https://developer.android.com/studio/publish/app-signing#sign-auto
Building and publishing 531
Testing the final build after manually signing
With an Android emulator running or a physical device connected to our machine, we can install a
production build of our application with the following command:
react-native run-android --variant=release
Aside from this, we can also publish alpha and beta versions of our app to a closed or open group
of testers before submitting it to the Play Store. This is covered in more detail in the next section.
Publishing to the Play Store
In order to publish and manage Android applications with Google Play Console
138
, we’ll need to
create a Google Developer account
139
which requires a one-time payment.
Once we launch the console, we can click Create Application to begin the process of submitting
an application to the Play Store.
The first few fields we need to fill out are the application’s default language and title.
Once completed, we land on the
Store listing
page. This page allows us to specify the content in
our app’s listing in the Play Store. Parameters include:
App description
Graphic assets such as screenshots, icons and banners
Application type and relevant category
Privacy policy link
The left-side menu contains links to other areas of our application such as setting up the price of a
paid application, its places of distribution, and its content rating.
The Android doc’s Launch Checklist
140
is a great resource on a number of best practices.
The App Releases link in the menu is where we can manage uploading the APK builds of our
application. We can roll out new builds in a number of different channels:
138
https://developer.android.com/distribute/console/
139
https://play.google.com/apps/publish/signup
140
https://developer.android.com/distribute/best-practices/launch/launch-checklist
Building and publishing 532
Internal test: Used to quickly publish builds for internal testing with a small number of testers.
Closed (alpha): Used to publish builds for closed testing. This is useful to test your application
with a larger number of privately invited testers.
Open (beta): Used to publish builds for open testing. Anybody can join this testing program
and provide private feedback to the author of the application.
Production: Used to publish final production builds to the Google Play Store.
With any of these build tracks, we can upload an APK file by clicking Manage and then Create
Release. In here, we have the option of using App Signing by Google Play instead of managing our
signing keys manually. Enabling this feature will mean that the signing key within the keystore used
for your application will be handled as an upload key. If we opt-in to this feature, we will have to
publish a new application if our key is lost or compromised.
There are a few more noteworthy parameters we need to specify. The first is the release name. This
is not visible to users and is used to distinguish separate releases in the Play Store. It is recommended
to use the version of the APK here or an internal code name relevant to this release.
The second is release notes. We use release notes to explain modifications to the app in this release.
After this, we can upload the final signed .apk build file (app-release.apk in the android/app/build/outputs/apk/
directory) and review our submission before rolling it out! If you decided to roll out a production
build, the application should show up in the Play Store in a few hours.
Handling Updates
Expo supports over-the-air (OTA) updates by default. Every standalone application built with
Expo will know how to reference updates to an application based on its URL (expo.io/user_-
name/APP_NAME). Once an Expo-built application is published to the iOS App Store or Google Play
Store and installed on a user’s mobile device, we can distribute any updates to the application using
the following command:
exp publish
This command allows us to publish directly with Expo. When the app is re-launched on a user’s
device, the new version of the application will be automatically downloaded and displayed. With
this, we can publish newer updates without distributing new build versions to app stores.
Publishing with Expo can also allow Android users to test and demo a CRNA application
without having to go through the Play Store submission process. You can refer to the
Appendix to get a better idea of how this works.
OTA updates only work when JavaScript code is modified. We cannot use this feature if we eject
from Expo to a regular React Native project. In this scenario, we can use a similar third-party tool like
Building and publishing 533
Microsoft’s CodePush
141
. This will allow us to still benefit from publishing changes to our JavaScript
bundle without deploying to an app store. However, we need to be careful to not publish newer
JavaScript code over-the-air that bridges functionality to native modules.
In order to comply with Apple’s review process, it is important to make sure that only specific
fixes/modifications that are in-line with the general direction of the application are included in
OTA updates. Section 3.3.2 in the Apple Developer Program License Agreement
142
states that only
code that does not change the initial purpose of the application and does not bypass any iOS security
features are permitted.
Summary
We began the journey of learning React Native by building a weather app in the first chapter of this
book. With each subsequent chapter, we learned about important React fundamentals, explored all
of the core components and APIs provided by the framework, and covered advanced topics such
as animations and gesture controls. We spent an entire chapter learning how to apply different
navigation patterns to an application and even investigated how to build custom native modules and
bridge to them ourselves. If you’ve reached this point of the book, you have all the tools you need
to both build and publish a fully functional and powerful React Native application. Give yourself a
pat on the back!
As the authors, we hope you enjoyed reading this book as much as we enjoyed writing it!
141
https://github.com/Microsoft/react-native-code-push
142
https://developer.apple.com/services-account/download?path=/Documentation/License_Agreements__Apple_Developer_Program/
Apple_Developer_Program_License_Agreement_20180604.pdf
Appendix
JavaScript Versions
JavaScript is the language of the web. It runs on many different browsers, like Google Chrome,
Firefox, Safari, Microsoft Edge, and Internet Explorer. React Native takes this one step further and
allows us to write JavaScript to communicate with native iOS and Android components.
Its widespread adoption as the internet’s client-side scripting language led to the formation of a
standards body which manages its specification. The specification is called ECMAScript or ES.
The 5th edition of the specification is called ES5. You can think of ES5 as a version of the JavaScript
programming language. The 6th edition, ES2015, was finalized in 2015 and is a significant update.
It contains a whole host of new features for JavaScript. JavaScript written in ES2015 is tangibly
different than JavaScript written in ES5.
ES2016, a much smaller update that builds on ES2015, was ratified in June 2016.
ES2015 is sometimes referred to as ES6. ES2016, in turn, is often referred to as ES7.
ES2015
Arrow functions
There are three ways to write arrow function bodies. For the examples below, let’s say we have an
array of city objects:
const cities = [
{ name: 'Cairo', pop: 7764700 },
{ name: 'Lagos', pop: 8029200 },
];
If we write an arrow function that spans multiple lines, we must use braces to delimit the function
body like this:
Appendix 535
const formattedPopulations = cities.map((city) => {
const popMM = (city.pop / 1000000).toFixed(2);
return popMM + ' million';
});
console.log(formattedPopulations);
// -> [ "7.76 million", "8.03 million" ]
Note that we must also explicitly specify a return for the function.
However, if we write a function body that is only a single line (or single expression) we can use
parentheses to delimit it:
const formattedPopulations2 = cities.map((city) => (
(city.pop / 1000000).toFixed(2) + ' million'
));
Notably, we don’t use return as it’s implied.
Furthermore, if your function body is terse you can write it like so:
const pops = cities.map(city => city.pop);
console.log(pops);
// [ 7764700, 8029200 ]
The terseness of arrow functions is one of two reasons that we use them. Compare the one-liner
above to this:
const popsNoArrow = cities.map(function(city) { return city.pop });
Of greater benefit, though, is how arrow functions bind the this object.
The traditional JavaScript function declaration syntax (function () {}) will bind this in anonymous
functions to the global object. To illustrate the confusion this causes, consider the following example:
function printSong() {
console.log("Oops - The Global Object");
}
const jukebox = {
songs: [
{
title: "Wanna Be Startin' Somethin'",
artist: "Michael Jackson",
},
Appendix 536
{
title: "Superstar",
artist: "Madonna",
},
],
printSong: function (song) {
console.log(song.title + " - " + song.artist);
},
printSongs: function () {
// `this` bound to the object (OK)
this.songs.forEach(function(song) {
// `this` bound to global object (bad)
this.printSong(song);
});
},
}
jukebox.printSongs();
// > "Oops - The Global Object"
// > "Oops - The Global Object"
The method printSongs() iterates over this.songs with forEach(). In this context, this is bound
to the object (jukebox) as expected. However, the anonymous function passed to forEach() binds
its internal this to the global object. As such, this.printSong(song) calls the function declared at
the top of the example, not the method on jukebox.
JavaScript developers have traditionally used workarounds for this behavior, but arrow functions
solve the problem by capturing the this value of the enclosing context. Using an arrow function
for printSongs() has the expected result:
function printSong() {
console.log("Oops - The Global Object");
}
const jukebox = {
songs: [
{
title: "Wanna Be Startin' Somethin'",
artist: "Michael Jackson",
},
{
title: "Superstar",
artist: "Madonna",
Appendix 537
},
],
printSong: function (song) {
console.log(song.title + " - " + song.artist);
},
printSongs: function () {
this.songs.forEach((song) => {
// `this` bound to same `this` as `printSongs()` (`jukebox`)
this.printSong(song);
});
},
}
jukebox.printSongs();
// > "Wanna Be Startin' Somethin' - Michael Jackson"
// > "Superstar - Madonna"
For this reason, throughout the book we use arrow functions for all anonymous functions.
Classes
JavaScript is a prototype-based language where classes, which is common in many object-oriented
languages, were not used. However, ES2015 introduced a class declaration syntax. For example:
1 class Ball {
2 constructor(color) {
3 this.color = color;
4 }
5
6 details() {
7 return 'This ball is ' + this.color + '!';
8 }
9 }
10
11 class SoccerBall extends Ball {
12 kick() {
13 return 'This ' + this.color + 'soccer ball is kicked!';
14 }
15 }
This isn’t a brand new JavaScript model, but only a simpler way to define object oriented structures
instead of using prototypal-based inheritance. For context, let’s take a look at how this would
probably look like without using a class definition:
Appendix 538
1 function Ball(color) {
2 this.color = color;
3 }
4
5 Ball.prototype.details = function details() {
6 return 'This ball is ' + this.color + '!';
7 };
8
9 function SoccerBall(color) {
10 Ball.call(this, color);
11 }
12
13 SoccerBall.prototype = Object.create(Ball.prototype);
14 SoccerBall.prototype.constructor = Ball;
15
16 SoccerBall.prototype.kick = function () {
17 return 'This ' + this.color + 'soccer ball is kicked!';
18 }
We won’t be going into more detail explaining object oriented paradigms and structures in
JavaScript, but it’s important to realize that creating objects with properties can be simpler with
classes. The important thing to note here is that we use this exact same model to create our React
Native components.
If you’d like to learn more about ES6 classes, refer to the docs on MDN
143
.
Shorthand property names
In ES5, all objects were required to have explicit key and value declarations:
const getState = () => {};
const dispatch = () => {};
const explicit = {
getState: getState,
dispatch: dispatch,
};
In ES2015, you can use this terser syntax whenever the property name and variable name are the
same:
143
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
Appendix 539
const getState = () => {};
const dispatch = () => {};
const implicit = {
getState,
dispatch,
};
Lots of open source libraries use this syntax, so it’s good to be familiar with it. But whether you use
it in your own code is a matter of stylistic preference.
Destructuring Assignments
For arrays
In ES5, extracting and assigning multiple elements from an array looked like this:
var fruits = [ 'apples', 'bananas', 'oranges' ];
var fruit1 = fruits[0];
var fruit2 = fruits[1];
In ES6, we can use the destructuring syntax to accomplish the same task like this:
const [ veg1, veg2 ] = [ 'asparagus', 'broccoli', 'onion' ];
console.log(veg1); // -> 'asparagus'
console.log(veg2); // -> 'broccoli'
The variables in the array on the left are “matched” and assigned to the corresponding elements in
the array on the right. Note that 'onion' is ignored and has no variable bound to it.
For objects
We can do something similar for extracting object properties into variables:
Appendix 540
const smoothie = {
fats: [ 'avocado', 'peanut butter', 'greek yogurt' ],
liquids: [ 'almond milk' ],
greens: [ 'spinach' ],
fruits: [ 'blueberry', 'banana' ],
};
const { liquids, fruits } = smoothie;
console.log(liquids); // -> [ 'almond milk' ]
console.log(fruits); // -> [ 'blueberry', 'banana' ]
Parameter context matching
We can use these same principles to bind arguments inside a function to properties of an object
supplied as an argument:
const containsSpinach = ({ greens }) => {
if (greens.find(g => g === 'spinach')) {
return true;
} else {
return false;
}
};
containsSpinach(smoothie); // -> true
We can also do this with functional React components.
ReactElement
React Native allows us to build applications with a fake representation of the native views rendered
in our mobile device. A ReactElement is a representation of a rendered element.
Consider this JavaScript syntax:
React.createElement(Text, { style: { color: 'red' }},
'Hello, friend! I am a basic React Native component.'
)
Which can be represented in JSX as:
Appendix 541
<Text style={{ color: 'red' }}>
Hello, friend! I am a basic React Native component.
</Text>
The code readability is slightly improved in the latter example. This is exacerbated in a nested tree
structure:
React.createElement(View, {},
React.createElement(Text, { style: { color: 'red' }},
'Hello, friend! I am a basic React Native component.'
)
)
In JSX:
<View>
<Text style={{ color: 'red' }}>
Hello, friend! I am a basic React Native component.
</Text>
</View>
Overall, JSX presents a light abstraction over the JavaScript version, yet the legibility benefits are
huge. Readability boosts our app’s longevity and makes it easier to onboard new developers.
Handling Events in React Native
Using bind statements within a render() method and property initializers aren’t the only ways to
handle events. We can also take care of binding our event handlers in a class constructor:
export default class SearchInput extends React.Component {
constructor() {
super();
this.handleChangeText = this.handleChangeText.bind(this);
}
handleChangeText(newLocation) {
// We need to do something with newLocation
}
render() {
Appendix 542
const { placeholder } = this.props;
return (
<TextInput
placeholder={placeholder}
placeholderTextColor="white"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
/>
);
}
}
Instead of using a constructor to bind our method, we can also also leverage ES6 arrow syntax to
achieve the same effect:
export default class SearchInput extends React.Component {
handleChangeText(newLocation) {
// We need to do something with newLocation
}
render() {
const { placeholder } = this.props;
return (
<TextInput
placeholder={placeholder}
placeholderTextColor="white"
style={styles.textInput}
clearButtonMode="always"
onChangeText={text => this.handleChangeText(text)}
/>
);
}
}
Notice how this simplifies our syntax where we don’t need to continously set up bind for each of
our event handlers. We’re specifically using ES6 arrow syntax to pass in the callback:
onChangeText={text => this.handleChangeText(text)}
Appendix 543
In most cases this is just fine, but it’s important to realize that this callback will instantiate every
time TextInput here is rendered. This will also be the case if we use bind statements within our
component JSX like we did previously. In most applications, this is unlikely to pose any noticeable
performance issues due to additional re-rendering. However, binding our member methods within
the constructor actually prevents this from happening.
This is where using property initializers can come in handy:
export default class SearchInput extends React.Component {
handleChangeText = newLocation => {
// We need to do something with newLocation
}
render() {
const { placeholder } = this.props;
return (
<TextInput
placeholder={placeholder}
placeholderTextColor="white"
style={styles.textInput}
clearButtonMode="always"
onChangeText={this.handleChangeText}
/>
);
}
}
By using this pattern, we can remove some boilerplate within our constructor method as well as
handle events in a cleaner fashion without causing additional re-renders.
Higher-Order Components
A Higher-Order Component (or HOC, for short) sounds complex, but the idea is simple: we
want a way to add common functionality (e.g data fetching or drag-and-drop) to many different
components. To do this, we write a function that takes an existing component and wraps it in
an enhanced component. Instead of changing the code of original component, a higher-order
component allows us to change the functionality by controlling how and when we show the original
component.
In code, a HOC is conceptually straightforward as well. To create a HOC, we’ll create a function
that accepts a component to wrap:
Appendix 544
const Enhance = OriginalComponent => {
return props => <OriginalComponent {...props} />;
};
It looks like there is a lot going on in the Enhance function, but it’s pretty simple. The function accepts
an OriginalComponent argument and returns a stateless component function.
JSX spread syntax
We cover spread syntax, {...props}, in the “React Fundamentals” chapter, but we haven’t
mentioned we can also use it for component props. Instead of having to know all of the
key-value pairs in the props, the spread syntax takes each of the props and sets them as
key-value pairs automatically.
For instance, if we have a props object that has two keys:
const props = {msg: "Hello", recipient: "World"}
In spread-syntax, JSX will make the resulting examples equivalent:
<Component {...props} />
<Component msg={"Hello"} recipient={"World"} />
The HOC can also return a class component:
const Enhance = OriginalComponent => {
return class extends React.Component {
render() {
return <OriginalComponent {...this.props} />;
}
};
};
Providing a class name is optional. It’s generally a good idea to provide a meaningful name
specific to the purpose of the HOC, but in this case we don’t know what the example HOC
does so we omitted the class name.
Notice that we can return whatever we want in our HOC as the render() value. To display the
original component, we just have to return it as a React component in JSX (as we do above). We
could instead modify the props of the original component before rendering it, or we could display a
completely different component based on the state of our enhanced component.
To apply our HOC to existing components, we can call it with the original component to get the
enhanced component.
Appendix 545
const EnhancedComponent = Enhance(OriginalComponent);
We can then use it anywhere we could use the original:
...
render() {
return <EnhancedComponent />
}
...
Publishing with Expo
Instead of ejecting a CRNA application and going through the process of building and publishing
standalone native builds manually, we also have the option to publish with Expo. This can be done
using the following command:
exp publish
Expo will take a few minutes compiling JavaScript bundles in production mode before deploying it to
the platform. Once completed, a URL will be provided that looks like https://exp.host/@fullstackio/contact-list.
You can send this link to any user who can open your application directly through the Expo Client
app.
One benefit of this approach is that users can access our application without us having to set
up standalone builds. This means we don’t have to take the steps to create and deploy native
iOS/Android builds to the app stores. The only requirement is that users have the Expo Client
application installed on their device. Every sample application in this book has been published with
Expo.
A disadvantage of relying on Expo Client for application distribution is that it only works for users
with Android devices. Users with iOS devices are significantly limited in accessing unauthorized
applications. They need to be logged in to the same Expo account that has published the project
in order to open and preview the application through the Client app. Only Android users have the
capability to open any Expo application published to the platform using their Client app. This means
that we only have the option of deploying a native build to the iOS App Store to allow other iOS
users to test and view our React Native application.
Changelog
This document highlights the changes for each version of Fullstack React Native.
Be sure to check there to ensure that you have the latest revision.
Revision 5 - 2018-07-26
Revision 5 - Adds Native Modules chapters to the book
Revision 4 - 2018-07-20
Revision 4 - Adds Publishing & Gestures chapters to the book
Revision 3 - 2018-05-10
Revision 3 - Adds Animation chapter to the book
Revision 2 - 2018-02-28
Adds Navigation chapter to the book
Revision 1 - 2017-12-06
Initial version of the book